summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/installation/installing-virtualbox-guest.xml5
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml14
-rw-r--r--nixos/lib/test-driver/test-driver.py65
-rw-r--r--nixos/modules/config/fonts/corefonts.nix36
-rw-r--r--nixos/modules/config/fonts/fontconfig-ultimate.nix86
-rw-r--r--nixos/modules/installer/tools/nixos-rebuild.sh9
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/programs/ssh.nix2
-rw-r--r--nixos/modules/programs/zsh/zsh-syntax-highlighting.nix4
-rw-r--r--nixos/modules/rename.nix10
-rw-r--r--nixos/modules/services/databases/redis.nix8
-rw-r--r--nixos/modules/services/misc/redmine.nix15
-rw-r--r--nixos/modules/services/misc/zoneminder.nix2
-rw-r--r--nixos/modules/services/networking/dnsdist.nix5
-rw-r--r--nixos/modules/services/networking/networkmanager.nix22
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix134
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/xmonad.nix2
-rw-r--r--nixos/modules/system/boot/systemd-unit-options.nix2
-rw-r--r--nixos/tests/all-tests.nix15
-rw-r--r--nixos/tests/caddy.nix57
-rw-r--r--nixos/tests/cadvisor.nix23
-rw-r--r--nixos/tests/cassandra.nix134
-rw-r--r--nixos/tests/certmgr.nix28
-rw-r--r--nixos/tests/cfssl.nix8
-rw-r--r--nixos/tests/cjdns.nix47
-rw-r--r--nixos/tests/cloud-init.nix13
-rw-r--r--nixos/tests/colord.nix18
-rw-r--r--nixos/tests/couchdb.nix56
-rw-r--r--nixos/tests/dnscrypt-proxy.nix12
-rw-r--r--nixos/tests/docker-edge.nix22
-rw-r--r--nixos/tests/docker.nix22
-rw-r--r--nixos/tests/documize.nix56
-rw-r--r--nixos/tests/firefox.nix36
-rw-r--r--nixos/tests/flatpak-builder.nix20
-rw-r--r--nixos/tests/flatpak.nix26
-rw-r--r--nixos/tests/fontconfig-default-fonts.nix15
-rw-r--r--nixos/tests/fwupd.nix21
-rw-r--r--nixos/tests/gdk-pixbuf.nix21
-rw-r--r--nixos/tests/gjs.nix19
-rw-r--r--nixos/tests/glib-networking.nix17
-rw-r--r--nixos/tests/gnome-photos.nix42
-rw-r--r--nixos/tests/grafana.nix48
-rw-r--r--nixos/tests/graphene.nix18
-rw-r--r--nixos/tests/initrd-network-ssh/default.nix38
-rw-r--r--nixos/tests/installed-tests/colord.nix5
-rw-r--r--nixos/tests/installed-tests/default.nix80
-rw-r--r--nixos/tests/installed-tests/flatpak-builder.nix14
-rw-r--r--nixos/tests/installed-tests/flatpak.nix19
-rw-r--r--nixos/tests/installed-tests/fwupd.nix12
-rw-r--r--nixos/tests/installed-tests/gcab.nix5
-rw-r--r--nixos/tests/installed-tests/gdk-pixbuf.nix13
-rw-r--r--nixos/tests/installed-tests/gjs.nix6
-rw-r--r--nixos/tests/installed-tests/glib-networking.nix5
-rw-r--r--nixos/tests/installed-tests/gnome-photos.nix35
-rw-r--r--nixos/tests/installed-tests/graphene.nix5
-rw-r--r--nixos/tests/installed-tests/libgdata.nix11
-rw-r--r--nixos/tests/installed-tests/libxmlb.nix5
-rw-r--r--nixos/tests/installed-tests/ostree.nix23
-rw-r--r--nixos/tests/installed-tests/xdg-desktop-portal.nix5
-rw-r--r--nixos/tests/libgdata.nix21
-rw-r--r--nixos/tests/libxmlb.nix17
-rw-r--r--nixos/tests/loki.nix18
-rw-r--r--nixos/tests/matrix-synapse.nix20
-rw-r--r--nixos/tests/moodle.nix8
-rw-r--r--nixos/tests/morty.nix10
-rw-r--r--nixos/tests/ostree.nix21
-rw-r--r--nixos/tests/packagekit.nix10
-rw-r--r--nixos/tests/pgjwt.nix23
-rw-r--r--nixos/tests/powerdns.nix7
-rw-r--r--nixos/tests/pppd.nix12
-rw-r--r--nixos/tests/prometheus-exporters.nix224
-rw-r--r--nixos/tests/radarr.nix8
-rw-r--r--nixos/tests/redis.nix14
-rw-r--r--nixos/tests/redmine.nix13
-rw-r--r--nixos/tests/roundcube.nix12
-rw-r--r--nixos/tests/rss2email.nix16
-rw-r--r--nixos/tests/shiori.nix76
-rw-r--r--nixos/tests/signal-desktop.nix12
-rw-r--r--nixos/tests/smokeping.nix16
-rw-r--r--nixos/tests/snapper.nix32
-rw-r--r--nixos/tests/strongswan-swanctl.nix6
-rw-r--r--nixos/tests/telegraf.nix8
-rw-r--r--nixos/tests/trickster.nix30
-rw-r--r--nixos/tests/udisks2.nix44
-rw-r--r--nixos/tests/upnp.nix20
-rw-r--r--nixos/tests/xautolock.nix12
-rw-r--r--nixos/tests/xdg-desktop-portal.nix17
-rw-r--r--nixos/tests/yabar.nix14
-rw-r--r--nixos/tests/zookeeper.nix22
90 files changed, 1181 insertions, 1082 deletions
diff --git a/nixos/doc/manual/installation/installing-virtualbox-guest.xml b/nixos/doc/manual/installation/installing-virtualbox-guest.xml
index 5c86eacfbf4..0ba909fa953 100644
--- a/nixos/doc/manual/installation/installing-virtualbox-guest.xml
+++ b/nixos/doc/manual/installation/installing-virtualbox-guest.xml
@@ -49,6 +49,11 @@
   </listitem>
   <listitem>
    <para>
+    Click on Settings / Display / Screen and select VBoxVGA as Graphics Controller
+   </para>
+  </listitem>
+  <listitem>
+   <para>
     Save the settings, start the virtual machine, and continue installation
     like normal
    </para>
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index c697b7ee047..495dbc8859b 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -494,6 +494,20 @@
      </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term>
+     <option>--use-remote-sudo</option>
+    </term>
+    <listitem>
+     <para>
+      When set, nixos-rebuild prefixes remote commands that run on
+      the <option>--build-host</option> and <option>--target-host</option>
+      systems with <command>sudo</command>. Setting this option allows
+      deploying as a non-root user.
+     </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
 
   <para>
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index 45b7e229a5c..c8940d78af4 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -4,7 +4,9 @@ from contextlib import contextmanager
 from xml.sax.saxutils import XMLGenerator
 import _thread
 import atexit
+import json
 import os
+import ptpython.repl
 import pty
 import queue
 import re
@@ -15,7 +17,6 @@ import sys
 import tempfile
 import time
 import unicodedata
-import ptpython.repl
 
 CHAR_TO_KEY = {
     "A": "shift-a",
@@ -305,7 +306,7 @@ class Machine:
             if state == "inactive":
                 status, jobs = self.systemctl("list-jobs --full 2>&1", user)
                 if "No jobs" in jobs:
-                    info = self.get_unit_info(unit)
+                    info = self.get_unit_info(unit, user)
                     if info["ActiveState"] == state:
                         raise Exception(
                             (
@@ -318,7 +319,11 @@ class Machine:
     def get_unit_info(self, unit, user=None):
         status, lines = self.systemctl('--no-pager show "{}"'.format(unit), user)
         if status != 0:
-            return None
+            raise Exception(
+                'retrieving systemctl info for unit "{}" {} failed with exit code {}'.format(
+                    unit, "" if user is None else 'under user "{}"'.format(user), status
+                )
+            )
 
         line_pattern = re.compile(r"^([^=]+)=(.*)$")
 
@@ -344,6 +349,18 @@ class Machine:
             )
         return self.execute("systemctl {}".format(q))
 
+    def require_unit_state(self, unit, require_state="active"):
+        with self.nested(
+            "checking if unit ‘{}’ has reached state '{}'".format(unit, require_state)
+        ):
+            info = self.get_unit_info(unit)
+            state = info["ActiveState"]
+            if state != require_state:
+                raise Exception(
+                    "Expected unit ‘{}’ to to be in state ".format(unit)
+                    + "'active' but it is in state ‘{}’".format(state)
+                )
+
     def execute(self, command):
         self.connect()
 
@@ -494,6 +511,11 @@ class Machine:
             if ret.returncode != 0:
                 raise Exception("Cannot convert screenshot")
 
+    def dump_tty_contents(self, tty):
+        """Debugging: Dump the contents of the TTY<n>
+        """
+        self.execute("fold -w 80 /dev/vcs{} | systemd-cat".format(tty))
+
     def get_screen_text(self):
         if shutil.which("tesseract") is None:
             raise Exception("get_screen_text used but enableOCR is false")
@@ -588,7 +610,7 @@ class Machine:
             stdin=subprocess.DEVNULL,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
-            shell=False,
+            shell=True,
             cwd=self.state_dir,
             env=environment,
         )
@@ -597,7 +619,7 @@ class Machine:
 
         def process_serial_output():
             for line in self.process.stdout:
-                line = line.decode().replace("\r", "").rstrip()
+                line = line.decode("unicode_escape").replace("\r", "").rstrip()
                 eprint("{} # {}".format(self.name, line))
                 self.logger.enqueue({"msg": line, "machine": self.name})
 
@@ -611,14 +633,14 @@ class Machine:
         self.log("QEMU running (pid {})".format(self.pid))
 
     def shutdown(self):
-        if self.booted:
+        if not self.booted:
             return
 
         self.shell.send("poweroff\n".encode())
         self.wait_for_shutdown()
 
     def crash(self):
-        if self.booted:
+        if not self.booted:
             return
 
         self.log("forced crash")
@@ -642,9 +664,38 @@ class Machine:
                 if status == 0:
                     return
 
+    def get_window_names(self):
+        return self.succeed(
+            r"xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'"
+        ).splitlines()
+
+    def wait_for_window(self, regexp):
+        pattern = re.compile(regexp)
+
+        def window_is_visible(last_try):
+            names = self.get_window_names()
+            if last_try:
+                self.log(
+                    "Last chance to match {} on the window list,".format(regexp)
+                    + " which currently contains: "
+                    + ", ".join(names)
+                )
+            return any(pattern.search(name) for name in names)
+
+        with self.nested("Waiting for a window to appear"):
+            retry(window_is_visible)
+
     def sleep(self, secs):
         time.sleep(secs)
 
+    def forward_port(self, host_port=8080, guest_port=80):
+        """Forward a TCP port on the host to a TCP port on the guest.
+        Useful during interactive testing.
+        """
+        self.send_monitor_command(
+            "hostfwd_add tcp::{}-:{}".format(host_port, guest_port)
+        )
+
     def block(self):
         """Make the machine unreachable by shutting down eth1 (the multicast
         interface used to talk to the other VMs).  We keep eth0 up so that
diff --git a/nixos/modules/config/fonts/corefonts.nix b/nixos/modules/config/fonts/corefonts.nix
deleted file mode 100644
index b9f69879a10..00000000000
--- a/nixos/modules/config/fonts/corefonts.nix
+++ /dev/null
@@ -1,36 +0,0 @@
-# This module is deprecated, since you can just say ‘fonts.fonts = [
-# pkgs.corefonts ];’ instead.
-
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-{
-
-  options = {
-
-    fonts = {
-
-      enableCoreFonts = mkOption {
-        visible = false;
-        default = false;
-        description = ''
-          Whether to include Microsoft's proprietary Core Fonts.  These fonts
-          are redistributable, but only verbatim, among other restrictions.
-          See <link xlink:href="http://corefonts.sourceforge.net/eula.htm"/>
-          for details.
-       '';
-      };
-
-    };
-
-  };
-
-
-  config = mkIf config.fonts.enableCoreFonts {
-
-    fonts.fonts = [ pkgs.corefonts ];
-
-  };
-
-}
diff --git a/nixos/modules/config/fonts/fontconfig-ultimate.nix b/nixos/modules/config/fonts/fontconfig-ultimate.nix
deleted file mode 100644
index 84d90899dff..00000000000
--- a/nixos/modules/config/fonts/fontconfig-ultimate.nix
+++ /dev/null
@@ -1,86 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let cfg = config.fonts.fontconfig.ultimate;
-
-    latestVersion  = pkgs.fontconfig.configVersion;
-
-    # The configuration to be included in /etc/font/
-    confPkg = pkgs.runCommand "font-ultimate-conf" { preferLocalBuild = true; } ''
-      support_folder=$out/etc/fonts/conf.d
-      latest_folder=$out/etc/fonts/${latestVersion}/conf.d
-
-      mkdir -p $support_folder
-      mkdir -p $latest_folder
-
-      # fontconfig ultimate substitutions
-      ${optionalString (cfg.substitutions != "none") ''
-      ln -s ${pkgs.fontconfig-ultimate}/etc/fonts/presets/${cfg.substitutions}/*.conf \
-            $support_folder
-      ln -s ${pkgs.fontconfig-ultimate}/etc/fonts/presets/${cfg.substitutions}/*.conf \
-            $latest_folder
-      ''}
-
-      # fontconfig ultimate various configuration files
-      ln -s ${pkgs.fontconfig-ultimate}/etc/fonts/conf.d/*.conf \
-            $support_folder
-      ln -s ${pkgs.fontconfig-ultimate}/etc/fonts/conf.d/*.conf \
-            $latest_folder
-    '';
-
-in
-{
-
-  options = {
-
-    fonts = {
-
-      fontconfig = {
-
-        ultimate = {
-          enable = mkOption {
-            type = types.bool;
-            default = false;
-            description = ''
-              Enable fontconfig-ultimate settings (formerly known as
-              Infinality). Besides the customizable settings in this NixOS
-              module, fontconfig-ultimate also provides many font-specific
-              rendering tweaks.
-            '';
-          };
-
-          substitutions = mkOption {
-            type = types.enum ["free" "combi" "ms" "none"];
-            default = "free";
-            description = ''
-              Font substitutions to replace common Type 1 fonts with nicer
-              TrueType fonts. <literal>free</literal> uses free fonts,
-              <literal>ms</literal> uses Microsoft fonts,
-              <literal>combi</literal> uses a combination, and
-              <literal>none</literal> disables the substitutions.
-            '';
-          };
-
-          preset = mkOption {
-            type = types.enum ["ultimate1" "ultimate2" "ultimate3" "ultimate4" "ultimate5" "osx" "windowsxp"];
-            default = "ultimate3";
-            description = ''
-              FreeType rendering settings preset. Any of the presets may be
-              customized by setting environment variables.
-            '';
-          };
-        };
-      };
-    };
-
-  };
-
-  config = mkIf (config.fonts.fontconfig.enable && cfg.enable) {
-
-    fonts.fontconfig.confPackages = [ confPkg ];
-    environment.variables.INFINALITY_FT = cfg.preset;
-
-  };
-
-}
diff --git a/nixos/modules/installer/tools/nixos-rebuild.sh b/nixos/modules/installer/tools/nixos-rebuild.sh
index 891f374df53..c53dc1000c4 100644
--- a/nixos/modules/installer/tools/nixos-rebuild.sh
+++ b/nixos/modules/installer/tools/nixos-rebuild.sh
@@ -90,6 +90,11 @@ while [ "$#" -gt 0 ]; do
         targetHost="$1"
         shift 1
         ;;
+      --use-remote-sudo)
+        # note the trailing space
+        maybeSudo="sudo "
+        shift 1
+        ;;
       *)
         echo "$0: unknown option \`$i'"
         exit 1
@@ -97,10 +102,6 @@ while [ "$#" -gt 0 ]; do
     esac
 done
 
-if [ -n "$SUDO_USER" ]; then
-    maybeSudo="sudo "
-fi
-
 if [ -z "$buildHost" -a -n "$targetHost" ]; then
     buildHost="$targetHost"
 fi
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 24912c27245..f7c66166c5c 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1,9 +1,7 @@
 [
   ./config/debug-info.nix
-  ./config/fonts/corefonts.nix
   ./config/fonts/fontconfig.nix
   ./config/fonts/fontconfig-penultimate.nix
-  ./config/fonts/fontconfig-ultimate.nix
   ./config/fonts/fontdir.nix
   ./config/fonts/fonts.nix
   ./config/fonts/ghostscript.nix
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 703975fd06c..80198990ed1 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -251,7 +251,7 @@ in
             ExecStart =
                 "${cfg.package}/bin/ssh-agent " +
                 optionalString (cfg.agentTimeout != null) ("-t ${cfg.agentTimeout} ") +
-                optionalString (cfg.agentPKCS11Whitelist != null) ("-P ${cfg.agentPKCS11Whitelist} ")
+                optionalString (cfg.agentPKCS11Whitelist != null) ("-P ${cfg.agentPKCS11Whitelist} ") +
                 "-a %t/ssh-agent";
             StandardOutput = "null";
             Type = "forking";
diff --git a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
index 7184e5d9b9a..c84d26a7921 100644
--- a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
+++ b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
@@ -81,7 +81,7 @@ in
     ];
 
     programs.zsh.interactiveShellInit = with pkgs;
-      lib.concatStringsSep "\n" ([
+      lib.mkAfter (lib.concatStringsSep "\n" ([
         "source ${zsh-syntax-highlighting}/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
       ] ++ optional (length(cfg.highlighters) > 0)
         "ZSH_HIGHLIGHT_HIGHLIGHTERS=(${concatStringsSep " " cfg.highlighters})"
@@ -95,6 +95,6 @@ in
             styles: design:
             "ZSH_HIGHLIGHT_STYLES[${styles}]='${design}'"
           ) cfg.styles)
-      );
+      ));
   };
 }
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index 886e2e83ba6..7d8cf55b827 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -234,6 +234,7 @@ with lib;
     (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.")
     (mkRemovedOptionModule [ "services" "zabbixServer" "dbPassword" ] "Use services.zabbixServer.database.passwordFile instead.")
     (mkRemovedOptionModule [ "systemd" "generator-packages" ] "Use systemd.packages instead.")
+    (mkRemovedOptionModule [ "fonts" "enableCoreFonts" ] "Use fonts.fonts = [ pkgs.corefonts ]; instead.")
 
     # ZSH
     (mkRenamedOptionModule [ "programs" "zsh" "enableSyntaxHighlighting" ] [ "programs" "zsh" "syntaxHighlighting" "enable" ])
@@ -291,5 +292,14 @@ with lib;
        (opt: mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] ''
          The prometheus exporters are now configured using `services.prometheus.exporters'.
          See the 18.03 release notes for more information.
+       '' ))
+
+    ++ (forEach [ "enable" "substitutions" "preset" ]
+       (opt: mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
+         The fonts.fontconfig.ultimate module and configuration is obsolete.
+         The repository has since been archived and activity has ceased.
+         https://github.com/bohoomil/fontconfig-ultimate/issues/171.
+         No action should be needed for font configuration, as the fonts.fontconfig
+         module is already used by default.
        '' ));
 }
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 5695eeaf74c..95128a641d9 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -185,10 +185,10 @@ in
   ###### implementation
 
   config = mkIf config.services.redis.enable {
-
-    boot.kernel.sysctl = {
-      "vm.nr_hugepages" = "0";
-    } // mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; };
+    boot.kernel.sysctl = (mkMerge [
+      { "vm.nr_hugepages" = "0"; }
+      ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
+    ]);
 
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts = [ cfg.port ];
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 24b9e27ac2d..bf9a6914a48 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -62,20 +62,11 @@ in
     services.redmine = {
       enable = mkEnableOption "Redmine";
 
-      # default to the 4.x series not forcing major version upgrade of those on the 3.x series
       package = mkOption {
         type = types.package;
-        default = if versionAtLeast config.system.stateVersion "19.03"
-          then pkgs.redmine_4
-          else pkgs.redmine
-        ;
-        defaultText = "pkgs.redmine";
-        description = ''
-          Which Redmine package to use. This defaults to version 3.x if
-          <literal>system.stateVersion &lt; 19.03</literal> and version 4.x
-          otherwise.
-        '';
-        example = "pkgs.redmine_4.override { ruby = pkgs.ruby_2_4; }";
+        default = pkgs.redmine;
+        description = "Which Redmine package to use.";
+        example = "pkgs.redmine.override { ruby = pkgs.ruby_2_4; }";
       };
 
       user = mkOption {
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index 3bff04e7127..d7f7324580c 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -265,7 +265,7 @@ in {
                 }
 
                 location /cache/ {
-                  alias /var/cache/${dirName};
+                  alias /var/cache/${dirName}/;
                 }
 
                 location ~ \.php$ {
diff --git a/nixos/modules/services/networking/dnsdist.nix b/nixos/modules/services/networking/dnsdist.nix
index 12eee136e63..8249da69bc1 100644
--- a/nixos/modules/services/networking/dnsdist.nix
+++ b/nixos/modules/services/networking/dnsdist.nix
@@ -46,11 +46,10 @@ in {
         RestartSec="1";
         DynamicUser = true;
         StartLimitInterval="0";
-        PrivateTmp=true;
         PrivateDevices=true;
-        CapabilityBoundingSet="CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID";
+        AmbientCapabilities="CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet="CAP_NET_BIND_SERVICE";
         ExecStart = "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}";
-        ProtectSystem="full";
         ProtectHome=true;
         RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
         LimitNOFILE="16384";
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 918bf891b10..90d1032c41b 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -456,15 +456,19 @@ in {
     };
 
     # Turn off NixOS' network management when networking is managed entirely by NetworkManager
-    networking = (mkIf (!delegateWireless) {
-      useDHCP = false;
-      # Use mkDefault to trigger the assertion about the conflict above
-      wireless.enable = mkDefault false;
-    }) // (mkIf cfg.enableStrongSwan {
-      networkmanager.packages = [ pkgs.networkmanager_strongswan ];
-    }) // (mkIf enableIwd {
-      wireless.iwd.enable = true;
-    });
+    networking = mkMerge [
+      (mkIf (!delegateWireless) {
+        useDHCP = false;
+      })
+
+      (mkIf cfg.enableStrongSwan {
+        networkmanager.packages = [ pkgs.networkmanager_strongswan ];
+      })
+
+      (mkIf enableIwd {
+        wireless.iwd.enable = true;
+      })
+    ];
 
     security.polkit.extraConfig = polkitConf;
 
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 99304d0e48a..f5a6051b4b5 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -6,6 +6,8 @@ let
 
   mainCfg = config.services.httpd;
 
+  runtimeDir = "/run/httpd";
+
   httpd = mainCfg.package.out;
 
   httpdConf = mainCfg.configFile;
@@ -27,41 +29,29 @@ let
 
   listenToString = l: "${l.ip}:${toString l.port}";
 
-  extraModules = attrByPath ["extraModules"] [] mainCfg;
-  extraForeignModules = filter isAttrs extraModules;
-  extraApacheModules = filter isString extraModules;
-
   allHosts = [mainCfg] ++ mainCfg.virtualHosts;
 
   enableSSL = any (vhost: vhost.enableSSL) allHosts;
 
+  enableUserDir = any (vhost: vhost.enableUserDir) allHosts;
 
-  # Names of modules from ${httpd}/modules that we want to load.
-  apacheModules =
-    [ # HTTP authentication mechanisms: basic and digest.
-      "auth_basic" "auth_digest"
-
-      # Authentication: is the user who he claims to be?
-      "authn_file" "authn_dbm" "authn_anon" "authn_core"
-
-      # Authorization: is the user allowed access?
-      "authz_user" "authz_groupfile" "authz_host" "authz_core"
-
-      # Other modules.
-      "ext_filter" "include" "log_config" "env" "mime_magic"
-      "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif"
-      "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs"
-      "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling"
-      "userdir" "alias" "rewrite" "proxy" "proxy_http"
-      "unixd" "cache" "cache_disk" "slotmem_shm" "socache_shmcb"
+  # NOTE: generally speaking order of modules is very important
+  modules =
+    [ # required apache modules our httpd service cannot run without
+      "authn_core" "authz_core"
+      "log_config"
+      "mime" "autoindex" "negotiation" "dir"
+      "alias" "rewrite"
+      "unixd" "slotmem_shm" "socache_shmcb"
       "mpm_${mainCfg.multiProcessingModule}"
-
-      # For compatibility with old configurations, the new module mod_access_compat is provided.
-      "access_compat"
     ]
     ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
     ++ optional enableSSL "ssl"
-    ++ extraApacheModules;
+    ++ optional enableUserDir "userdir"
+    ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
+    ++ optional mainCfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
+    ++ optional mainCfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
+    ++ mainCfg.extraModules;
 
 
   allDenied = "Require all denied";
@@ -85,20 +75,22 @@ let
 
 
   browserHacks = ''
-    BrowserMatch "Mozilla/2" nokeepalive
-    BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
-    BrowserMatch "RealPlayer 4\.0" force-response-1.0
-    BrowserMatch "Java/1\.0" force-response-1.0
-    BrowserMatch "JDK/1\.0" force-response-1.0
-    BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
-    BrowserMatch "^WebDrive" redirect-carefully
-    BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully
-    BrowserMatch "^gnome-vfs" redirect-carefully
+    <IfModule mod_setenvif.c>
+        BrowserMatch "Mozilla/2" nokeepalive
+        BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
+        BrowserMatch "RealPlayer 4\.0" force-response-1.0
+        BrowserMatch "Java/1\.0" force-response-1.0
+        BrowserMatch "JDK/1\.0" force-response-1.0
+        BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
+        BrowserMatch "^WebDrive" redirect-carefully
+        BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully
+        BrowserMatch "^gnome-vfs" redirect-carefully
+    </IfModule>
   '';
 
 
   sslConf = ''
-    SSLSessionCache shmcb:${mainCfg.stateDir}/ssl_scache(512000)
+    SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
 
     Mutex posixsem
 
@@ -239,13 +231,13 @@ let
 
     ServerRoot ${httpd}
 
-    DefaultRuntimeDir ${mainCfg.stateDir}/runtime
+    DefaultRuntimeDir ${runtimeDir}/runtime
 
-    PidFile ${mainCfg.stateDir}/httpd.pid
+    PidFile ${runtimeDir}/httpd.pid
 
     ${optionalString (mainCfg.multiProcessingModule != "prefork") ''
       # mod_cgid requires this.
-      ScriptSock ${mainCfg.stateDir}/cgisock
+      ScriptSock ${runtimeDir}/cgisock
     ''}
 
     <IfModule prefork.c>
@@ -264,13 +256,12 @@ let
     Group ${mainCfg.group}
 
     ${let
-        load = {name, path}: "LoadModule ${name}_module ${path}\n";
-        allModules = map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules
-          ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
-          ++ optional mainCfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
-          ++ optional mainCfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
-          ++ extraForeignModules;
-      in concatMapStrings load (unique allModules)
+        mkModule = module:
+          if isString module then { name = module; path = "${httpd}/modules/mod_${module}.so"; }
+          else if isAttrs module then { inherit (module) name path; }
+          else throw "Expecting either a string or attribute set including a name and path.";
+      in
+        concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules))
     }
 
     AddHandler type-map var
@@ -337,6 +328,7 @@ in
 
   imports = [
     (mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.")
+    (mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.")
   ];
 
   ###### interface
@@ -384,7 +376,12 @@ in
       extraModules = mkOption {
         type = types.listOf types.unspecified;
         default = [];
-        example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]'';
+        example = literalExample ''
+          [
+            "proxy_connect"
+            { name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.so"; }
+          ]
+        '';
         description = ''
           Additional Apache modules to be used.  These can be
           specified as a string in the case of modules distributed
@@ -431,16 +428,6 @@ in
         '';
       };
 
-      stateDir = mkOption {
-        type = types.path;
-        default = "/run/httpd";
-        description = ''
-          Directory for Apache's transient runtime state (such as PID
-          files).  It is created automatically.  Note that the default,
-          <filename>/run/httpd</filename>, is deleted at boot time.
-        '';
-      };
-
       virtualHosts = mkOption {
         type = types.listOf (types.submodule (
           { options = import ./per-server-options.nix {
@@ -595,6 +582,28 @@ in
         date.timezone = "${config.time.timeZone}"
       '';
 
+    services.httpd.extraModules = mkBefore [
+      # HTTP authentication mechanisms: basic and digest.
+      "auth_basic" "auth_digest"
+
+      # Authentication: is the user who he claims to be?
+      "authn_file" "authn_dbm" "authn_anon"
+
+      # Authorization: is the user allowed access?
+      "authz_user" "authz_groupfile" "authz_host"
+
+      # Other modules.
+      "ext_filter" "include" "env" "mime_magic"
+      "cern_meta" "expires" "headers" "usertrack" "setenvif"
+      "dav" "status" "asis" "info" "dav_fs"
+      "vhost_alias" "imagemap" "actions" "speling"
+      "proxy" "proxy_http"
+      "cache" "cache_disk"
+
+      # For compatibility with old configurations, the new module mod_access_compat is provided.
+      "access_compat"
+    ];
+
     systemd.services.httpd =
       { description = "Apache HTTPD";
 
@@ -611,12 +620,6 @@ in
 
         preStart =
           ''
-            mkdir -m 0750 -p ${mainCfg.stateDir}
-            [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir}
-
-            mkdir -m 0750 -p "${mainCfg.stateDir}/runtime"
-            [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime"
-
             mkdir -m 0700 -p ${mainCfg.logDir}
 
             # Get rid of old semaphores.  These tend to accumulate across
@@ -630,10 +633,13 @@ in
         serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}";
         serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop";
         serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful";
+        serviceConfig.Group = mainCfg.group;
         serviceConfig.Type = "forking";
-        serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid";
+        serviceConfig.PIDFile = "${runtimeDir}/httpd.pid";
         serviceConfig.Restart = "always";
         serviceConfig.RestartSec = "5s";
+        serviceConfig.RuntimeDirectory = "httpd httpd/runtime";
+        serviceConfig.RuntimeDirectoryMode = "0750";
       };
 
   };
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 8847acb0c60..899dd8665a2 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -14,7 +14,7 @@ let
   xserverWrapper = pkgs.writeScript "xserver-wrapper" ''
     #!/bin/sh
     ${concatMapStrings (n: "export ${n}=\"${getAttr n xEnv}\"\n") (attrNames xEnv)}
-    exec systemd-cat ${dmcfg.xserverBin} ${toString dmcfg.xserverArgs} "$@"
+    exec systemd-cat -t xserver-wrapper ${dmcfg.xserverBin} ${toString dmcfg.xserverArgs} "$@"
   '';
 
   Xsetup = pkgs.writeScript "Xsetup" ''
diff --git a/nixos/modules/services/x11/window-managers/xmonad.nix b/nixos/modules/services/x11/window-managers/xmonad.nix
index 0e131412276..30c59b88f82 100644
--- a/nixos/modules/services/x11/window-managers/xmonad.nix
+++ b/nixos/modules/services/x11/window-managers/xmonad.nix
@@ -86,7 +86,7 @@ in
           ${xmonadBin}
           waitPID=$!
         '' else ''
-          ${xmonad}/bin/xmonad &
+          systemd-cat -t xmonad ${xmonad}/bin/xmonad &
           waitPID=$!
         '';
       }];
diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix
index c1f2c98afcd..bee21f1a8f3 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/modules/system/boot/systemd-unit-options.nix
@@ -24,7 +24,7 @@ in rec {
       in
         if isList (head defs'')
         then concatLists defs''
-        else mergeOneOption loc defs';
+        else mergeEqualOption loc defs';
   };
 
   sharedOptions = {
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 5e7c8a7f4b5..9db505a27d4 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -48,7 +48,6 @@ in
   clickhouse = handleTest ./clickhouse.nix {};
   cloud-init = handleTest ./cloud-init.nix {};
   codimd = handleTest ./codimd.nix {};
-  colord = handleTest ./colord.nix {};
   containers-bridge = handleTest ./containers-bridge.nix {};
   containers-ephemeral = handleTest ./containers-ephemeral.nix {};
   containers-extra_veth = handleTest ./containers-extra_veth.nix {};
@@ -88,27 +87,20 @@ in
   firewall = handleTest ./firewall.nix {};
   fish = handleTest ./fish.nix {};
   flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
-  flatpak = handleTest ./flatpak.nix {};
-  flatpak-builder = handleTest ./flatpak-builder.nix {};
   fluentd = handleTest ./fluentd.nix {};
   fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {};
   fsck = handleTest ./fsck.nix {};
-  fwupd = handleTestOn ["x86_64-linux"] ./fwupd.nix {}; # libsmbios is unsupported on aarch64
-  gdk-pixbuf = handleTest ./gdk-pixbuf.nix {};
   gotify-server = handleTest ./gotify-server.nix {};
   gitea = handleTest ./gitea.nix {};
   gitlab = handleTest ./gitlab.nix {};
   gitolite = handleTest ./gitolite.nix {};
-  gjs = handleTest ./gjs.nix {};
-  glib-networking = handleTest ./glib-networking.nix {};
   glusterfs = handleTest ./glusterfs.nix {};
   gnome3-xorg = handleTest ./gnome3-xorg.nix {};
   gnome3 = handleTest ./gnome3.nix {};
-  gnome-photos = handleTest ./gnome-photos.nix {};
+  installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
   gocd-agent = handleTest ./gocd-agent.nix {};
   gocd-server = handleTest ./gocd-server.nix {};
   google-oslogin = handleTest ./google-oslogin {};
-  graphene = handleTest ./graphene.nix {};
   grafana = handleTest ./grafana.nix {};
   graphite = handleTest ./graphite.nix {};
   graylog = handleTest ./graylog.nix {};
@@ -149,8 +141,6 @@ in
   latestKernel.login = handleTest ./login.nix { latestKernel = true; };
   ldap = handleTest ./ldap.nix {};
   leaps = handleTest ./leaps.nix {};
-  libgdata = handleTest ./libgdata.nix {};
-  libxmlb = handleTest ./libxmlb.nix {};
   lidarr = handleTest ./lidarr.nix {};
   lightdm = handleTest ./lightdm.nix {};
   limesurvey = handleTest ./limesurvey.nix {};
@@ -216,7 +206,6 @@ in
   os-prober = handleTestOn ["x86_64-linux"] ./os-prober.nix {};
   osquery = handleTest ./osquery.nix {};
   osrm-backend = handleTest ./osrm-backend.nix {};
-  ostree = handleTest ./ostree.nix {};
   overlayfs = handleTest ./overlayfs.nix {};
   packagekit = handleTest ./packagekit.nix {};
   pam-oath-login = handleTest ./pam-oath-login.nix {};
@@ -255,6 +244,7 @@ in
   rxe = handleTest ./rxe.nix {};
   samba = handleTest ./samba.nix {};
   sddm = handleTest ./sddm.nix {};
+  shiori = handleTest ./shiori.nix {};
   signal-desktop = handleTest ./signal-desktop.nix {};
   simple = handleTest ./simple.nix {};
   slim = handleTest ./slim.nix {};
@@ -291,7 +281,6 @@ in
   wireguard-generated = handleTest ./wireguard/generated.nix {};
   wordpress = handleTest ./wordpress.nix {};
   xautolock = handleTest ./xautolock.nix {};
-  xdg-desktop-portal = handleTest ./xdg-desktop-portal.nix {};
   xfce = handleTest ./xfce.nix {};
   xfce4-14 = handleTest ./xfce4-14.nix {};
   xmonad = handleTest ./xmonad.nix {};
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
index ab9d2fbf4d1..fc10df0c79b 100644
--- a/nixos/tests/caddy.nix
+++ b/nixos/tests/caddy.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "caddy";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ xfix ];
@@ -50,33 +50,38 @@ import ./make-test.nix ({ pkgs, ... }: {
     etagSystem = "${nodes.webserver.config.system.build.toplevel}/fine-tune/child-1";
     justReloadSystem = "${nodes.webserver.config.system.build.toplevel}/fine-tune/child-2";
   in ''
-    my $url = 'http://localhost/example.html';
-    $webserver->waitForUnit("caddy");
-    $webserver->waitForOpenPort("80");
+    url = "http://localhost/example.html"
+    webserver.wait_for_unit("caddy")
+    webserver.wait_for_open_port("80")
 
-    sub checkEtag {
-      my $etag = $webserver->succeed(
-        'curl -v '.$url.' 2>&1 | sed -n -e "s/^< [Ee][Tt][Aa][Gg]: *//p"'
-      );
-      $etag =~ s/\r?\n$//;
-      my $httpCode = $webserver->succeed(
-        'curl -w "%{http_code}" -X HEAD -H \'If-None-Match: '.$etag.'\' '.$url
-      );
-      die "HTTP code is not 304" unless $httpCode == 304;
-      return $etag;
-    }
 
-    subtest "check ETag if serving Nix store paths", sub {
-      my $oldEtag = checkEtag;
-      $webserver->succeed("${etagSystem}/bin/switch-to-configuration test >&2");
-      $webserver->sleep(1); # race condition
-      my $newEtag = checkEtag;
-      die "Old ETag $oldEtag is the same as $newEtag" if $oldEtag eq $newEtag;
-    };
+    def check_etag(url):
+        etag = webserver.succeed(
+            "curl -v '{}' 2>&1 | sed -n -e \"s/^< [Ee][Tt][Aa][Gg]: *//p\"".format(url)
+        )
+        etag = etag.replace("\r\n", " ")
+        http_code = webserver.succeed(
+            "curl -w \"%{{http_code}}\" -X HEAD -H 'If-None-Match: {}' {}".format(etag, url)
+        )
+        assert int(http_code) == 304, "HTTP code is not 304"
+        return etag
 
-    subtest "config is reloaded on nixos-rebuild switch", sub {
-      $webserver->succeed("${justReloadSystem}/bin/switch-to-configuration test >&2");
-      $webserver->waitForOpenPort("8080");
-    };
+
+    with subtest("check ETag if serving Nix store paths"):
+        old_etag = check_etag(url)
+        webserver.succeed(
+            "${etagSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.sleep(1)
+        new_etag = check_etag(url)
+        assert old_etag != new_etag, "Old ETag {} is the same as {}".format(
+            old_etag, new_etag
+        )
+    
+    with subtest("config is reloaded on nixos-rebuild switch"):
+        webserver.succeed(
+            "${justReloadSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.wait_for_open_port("8080")
   '';
 })
diff --git a/nixos/tests/cadvisor.nix b/nixos/tests/cadvisor.nix
index e60bae4b700..60c04f14780 100644
--- a/nixos/tests/cadvisor.nix
+++ b/nixos/tests/cadvisor.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... } : {
+import ./make-test-python.nix ({ pkgs, ... } : {
   name = "cadvisor";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ offline ];
@@ -16,20 +16,19 @@ import ./make-test.nix ({ pkgs, ... } : {
     };
   };
 
-  testScript =
-    ''
-      startAll;
-      $machine->waitForUnit("cadvisor.service");
-      $machine->succeed("curl http://localhost:8080/containers/");
+  testScript =  ''
+      start_all()
+      machine.wait_for_unit("cadvisor.service")
+      machine.succeed("curl http://localhost:8080/containers/")
 
-      $influxdb->waitForUnit("influxdb.service");
+      influxdb.wait_for_unit("influxdb.service")
 
       # create influxdb database
-      $influxdb->succeed(q~
-        curl -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"
-      ~);
+      influxdb.succeed(
+          'curl -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"'
+      )
 
-      $influxdb->waitForUnit("cadvisor.service");
-      $influxdb->succeed("curl http://localhost:8080/containers/");
+      influxdb.wait_for_unit("cadvisor.service")
+      influxdb.succeed("curl http://localhost:8080/containers/")
     '';
 })
diff --git a/nixos/tests/cassandra.nix b/nixos/tests/cassandra.nix
index c55733c9be7..05607956a9d 100644
--- a/nixos/tests/cassandra.nix
+++ b/nixos/tests/cassandra.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 let
   # Change this to test a different version of Cassandra:
   testPackage = pkgs.cassandra;
@@ -9,13 +9,16 @@ let
   jmxRolesFile = ./cassandra-jmx-roles;
   jmxAuthArgs = "-u ${(builtins.elemAt jmxRoles 0).username} -pw ${(builtins.elemAt jmxRoles 0).password}";
   jmxPort = 7200;  # Non-standard port so it doesn't accidentally work
+  jmxPortStr = toString jmxPort;
 
-  # Would usually be assigned to 512M
+  # Would usually be assigned to 512M.
+  # Set it to a different value, so that we can check whether our config
+  # actually changes it.
   numMaxHeapSize = "400";
   getHeapLimitCommand = ''
-    nodetool info -p ${toString jmxPort} | grep "^Heap Memory" | awk \'{print $NF}\'
+    nodetool info -p ${jmxPortStr} | grep "^Heap Memory" | awk '{print $NF}'
   '';
-  checkHeapLimitCommand = ''
+  checkHeapLimitCommand = pkgs.writeShellScript "check-heap-limit.sh" ''
     [ 1 -eq "$(echo "$(${getHeapLimitCommand}) < ${numMaxHeapSize}" | ${pkgs.bc}/bin/bc)" ]
   '';
 
@@ -44,7 +47,10 @@ let
   };
 in
 {
-  name = "cassandra-ci";
+  name = "cassandra";
+  meta = {
+    maintainers = with lib.maintainers; [ johnazoidberg ];
+  };
 
   nodes = {
     cass0 = nodeCfg "192.168.1.1" {};
@@ -52,66 +58,74 @@ in
     cass2 = nodeCfg "192.168.1.3" { jvmOpts = [ "-Dcassandra.replace_address=cass1" ]; };
   };
 
-  testScript = let
-    jmxPortS = toString jmxPort;
-  in ''
+  testScript = ''
     # Check configuration
-    subtest "Timers exist", sub {
-      $cass0->succeed("systemctl list-timers | grep cassandra-full-repair.timer");
-      $cass0->succeed("systemctl list-timers | grep cassandra-incremental-repair.timer");
-    };
-    subtest "Can connect via cqlsh", sub {
-      $cass0->waitForUnit("cassandra.service");
-      $cass0->waitUntilSucceeds("nc -z cass0 9042");
-      $cass0->succeed("echo 'show version;' | cqlsh cass0");
-    };
-    subtest "Nodetool is operational", sub {
-      $cass0->waitForUnit("cassandra.service");
-      $cass0->waitUntilSucceeds("nc -z localhost ${jmxPortS}");
-      $cass0->succeed("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass0'");
-    };
-    subtest "Cluster name was set", sub {
-      $cass0->waitForUnit("cassandra.service");
-      $cass0->waitUntilSucceeds("nc -z localhost ${jmxPortS}");
-      $cass0->waitUntilSucceeds("nodetool describecluster -p ${jmxPortS} | grep 'Name: ${clusterName}'");
-    };
-    subtest "Heap limit set correctly", sub {
-      # Nodetool takes a while until it can display info
-      $cass0->waitUntilSucceeds('nodetool info -p ${jmxPortS}');
-      $cass0->succeed('${checkHeapLimitCommand}');
-    };
+    with subtest("Timers exist"):
+        cass0.succeed("systemctl list-timers | grep cassandra-full-repair.timer")
+        cass0.succeed("systemctl list-timers | grep cassandra-incremental-repair.timer")
+
+    with subtest("Can connect via cqlsh"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z cass0 9042")
+        cass0.succeed("echo 'show version;' | cqlsh cass0")
+
+    with subtest("Nodetool is operational"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.succeed("nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass0'")
+
+    with subtest("Cluster name was set"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.wait_until_succeeds(
+            "nodetool describecluster -p ${jmxPortStr} | grep 'Name: ${clusterName}'"
+        )
+
+    with subtest("Heap limit set correctly"):
+        # Nodetool takes a while until it can display info
+        cass0.wait_until_succeeds("nodetool info -p ${jmxPortStr}")
+        cass0.succeed("${checkHeapLimitCommand}")
 
     # Check cluster interaction
-    subtest "Bring up cluster", sub {
-      $cass1->waitForUnit("cassandra.service");
-      $cass1->waitUntilSucceeds("nodetool -p ${jmxPortS} ${jmxAuthArgs} status | egrep -c '^UN' | grep 2");
-      $cass0->succeed("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass1'");
-    };
+    with subtest("Bring up cluster"):
+        cass1.wait_for_unit("cassandra.service")
+        cass1.wait_until_succeeds(
+            "nodetool -p ${jmxPortStr} ${jmxAuthArgs} status | egrep -c '^UN' | grep 2"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass1'")
   '' + lib.optionalString testRemoteAuth ''
-    subtest "Remote authenticated jmx", sub {
-      # Doesn't work if not enabled
-      $cass0->waitUntilSucceeds("nc -z localhost ${jmxPortS}");
-      $cass1->fail("nc -z 192.168.1.1 ${toString jmxPort}");
-      $cass1->fail("nodetool -p ${jmxPortS} -h 192.168.1.1 status");
+    with subtest("Remote authenticated jmx"):
+        # Doesn't work if not enabled
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass1.fail("nc -z 192.168.1.1 ${jmxPortStr}")
+        cass1.fail("nodetool -p ${jmxPortStr} -h 192.168.1.1 status")
 
-      # Works if enabled
-      $cass1->waitUntilSucceeds("nc -z localhost ${toString jmxPort}");
-      $cass0->succeed("nodetool -p ${jmxPortS} -h 192.168.1.2 ${jmxAuthArgs} status");
-    };
+        # Works if enabled
+        cass1.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.succeed("nodetool -p ${jmxPortStr} -h 192.168.1.2 ${jmxAuthArgs} status")
   '' + ''
-    subtest "Break and fix node", sub {
-      $cass1->block;
-      $cass0->waitUntilSucceeds("nodetool status -p ${jmxPortS} --resolve-ip | egrep -c '^DN[[:space:]]+cass1'");
-      $cass0->succeed("nodetool status -p ${jmxPortS} | egrep -c '^UN'  | grep 1");
-      $cass1->unblock;
-      $cass1->waitUntilSucceeds("nodetool -p ${jmxPortS} ${jmxAuthArgs} status | egrep -c '^UN'  | grep 2");
-      $cass0->succeed("nodetool status -p ${jmxPortS} | egrep -c '^UN'  | grep 2");
-    };
-    subtest "Replace crashed node", sub {
-      $cass1->crash;
-      $cass2->waitForUnit("cassandra.service");
-      $cass0->waitUntilFails("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass1'");
-      $cass0->waitUntilSucceeds("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass2'");
-    };
+    with subtest("Break and fix node"):
+        cass1.block()
+        cass0.wait_until_succeeds(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep -c '^DN[[:space:]]+cass1'"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} | egrep -c '^UN'  | grep 1")
+        cass1.unblock()
+        cass1.wait_until_succeeds(
+            "nodetool -p ${jmxPortStr} ${jmxAuthArgs} status | egrep -c '^UN'  | grep 2"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} | egrep -c '^UN'  | grep 2")
+
+    with subtest("Replace crashed node"):
+        cass1.block()  # .crash() waits until it's fully shutdown
+        cass2.start()
+        cass0.wait_until_fails(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass1'"
+        )
+
+        cass2.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass2'"
+        )
   '';
 })
diff --git a/nixos/tests/certmgr.nix b/nixos/tests/certmgr.nix
index fe67833808c..cb69f35e862 100644
--- a/nixos/tests/certmgr.nix
+++ b/nixos/tests/certmgr.nix
@@ -3,7 +3,7 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 let
   mkSpec = { host, service ? null, action }: {
     inherit action;
@@ -123,17 +123,17 @@ in
       )));
     };
     testScript = ''
-      $machine->waitForUnit('cfssl.service');
-      $machine->waitUntilSucceeds('ls /tmp/decl.example.org-ca.pem');
-      $machine->waitUntilSucceeds('ls /tmp/decl.example.org-key.pem');
-      $machine->waitUntilSucceeds('ls /tmp/decl.example.org-cert.pem');
-      $machine->waitUntilSucceeds('ls /tmp/imp.example.org-ca.pem');
-      $machine->waitUntilSucceeds('ls /tmp/imp.example.org-key.pem');
-      $machine->waitUntilSucceeds('ls /tmp/imp.example.org-cert.pem');
-      $machine->waitForUnit('nginx.service');
-      $machine->succeed('[ "1" -lt "$(journalctl -u nginx | grep "Starting Nginx" | wc -l)" ]');
-      $machine->succeed('curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org');
-      $machine->succeed('curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org');
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("ls /tmp/decl.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /tmp/decl.example.org-key.pem")
+      machine.wait_until_succeeds("ls /tmp/decl.example.org-cert.pem")
+      machine.wait_until_succeeds("ls /tmp/imp.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /tmp/imp.example.org-key.pem")
+      machine.wait_until_succeeds("ls /tmp/imp.example.org-cert.pem")
+      machine.wait_for_unit("nginx.service")
+      assert 1 < int(machine.succeed('journalctl -u nginx | grep "Starting Nginx" | wc -l'))
+      machine.succeed("curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org")
+      machine.succeed("curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org")
     '';
   };
 
@@ -143,8 +143,8 @@ in
       test = mkSpec { host = "command.example.org"; action = "touch /tmp/command.executed"; };
     };
     testScript = ''
-      $machine->waitForUnit('cfssl.service');
-      $machine->waitUntilSucceeds('stat /tmp/command.executed');
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("stat /tmp/command.executed")
     '';
   };
 
diff --git a/nixos/tests/cfssl.nix b/nixos/tests/cfssl.nix
index 513ed8c4574..e291fc285fb 100644
--- a/nixos/tests/cfssl.nix
+++ b/nixos/tests/cfssl.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cfssl";
 
   machine = { config, lib, pkgs, ... }:
@@ -60,8 +60,8 @@ import ./make-test.nix ({ pkgs, ...} : {
     });
   in
     ''
-      $machine->waitForUnit('cfssl.service');
-      $machine->waitUntilSucceeds('${cfsslrequest}');
-      $machine->succeed('ls /tmp/certificate-key.pem');
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("${cfsslrequest}")
+      machine.succeed("ls /tmp/certificate-key.pem")
     '';
 })
diff --git a/nixos/tests/cjdns.nix b/nixos/tests/cjdns.nix
index 6660eecf05b..d72236d415d 100644
--- a/nixos/tests/cjdns.nix
+++ b/nixos/tests/cjdns.nix
@@ -17,7 +17,7 @@ let
 
 in
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cjdns";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ ehmry ];
@@ -83,36 +83,39 @@ import ./make-test.nix ({ pkgs, ...} : {
 
   testScript =
     ''
-      startAll;
+      import re
 
-      $alice->waitForUnit("cjdns.service");
-      $bob->waitForUnit("cjdns.service");
-      $carol->waitForUnit("cjdns.service");
+      start_all()
 
-      sub cjdnsIp {
-          my ($machine) = @_;
-          my $ip = (split /[ \/]+/, $machine->succeed("ip -o -6 addr show dev tun0"))[3];
-          $machine->log("has ip $ip");
-          return $ip;
-      }
+      alice.wait_for_unit("cjdns.service")
+      bob.wait_for_unit("cjdns.service")
+      carol.wait_for_unit("cjdns.service")
 
-      my $aliceIp6 = cjdnsIp $alice;
-      my $bobIp6   = cjdnsIp $bob;
-      my $carolIp6 = cjdnsIp $carol;
+
+      def cjdns_ip(machine):
+          res = machine.succeed("ip -o -6 addr show dev tun0")
+          ip = re.split("\s+|/", res)[3]
+          machine.log("has ip {}".format(ip))
+          return ip
+
+
+      alice_ip6 = cjdns_ip(alice)
+      bob_ip6 = cjdns_ip(bob)
+      carol_ip6 = cjdns_ip(carol)
 
       # ping a few times each to let the routing table establish itself
 
-      $alice->succeed("ping -c 4 $carolIp6");
-      $bob->succeed("ping -c 4 $carolIp6");
+      alice.succeed("ping -c 4 {}".format(carol_ip6))
+      bob.succeed("ping -c 4 {}".format(carol_ip6))
 
-      $carol->succeed("ping -c 4 $aliceIp6");
-      $carol->succeed("ping -c 4 $bobIp6");
+      carol.succeed("ping -c 4 {}".format(alice_ip6))
+      carol.succeed("ping -c 4 {}".format(bob_ip6))
 
-      $alice->succeed("ping -c 4 $bobIp6");
-      $bob->succeed("ping -c 4 $aliceIp6");
+      alice.succeed("ping -c 4 {}".format(bob_ip6))
+      bob.succeed("ping -c 4 {}".format(alice_ip6))
 
-      $alice->waitForUnit("httpd.service");
+      alice.wait_for_unit("httpd.service")
 
-      $bob->succeed("curl --fail -g http://[$aliceIp6]");
+      bob.succeed("curl --fail -g http://[{}]".format(alice_ip6))
     '';
 })
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
index 516d29c9036..aafa6e24e84 100644
--- a/nixos/tests/cloud-init.nix
+++ b/nixos/tests/cloud-init.nix
@@ -3,7 +3,7 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
@@ -30,6 +30,7 @@ let
       '';
   };
 in makeTest {
+  name = "cloud-init";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ lewo ];
   };
@@ -40,10 +41,12 @@ in makeTest {
       services.cloud-init.enable = true;
     };
   testScript = ''
-     $machine->start;
-     $machine->waitForUnit("cloud-init.service");
-     $machine->succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'");
+      machine.start()
+      machine.wait_for_unit("cloud-init.service")
+      machine.succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'")
 
-     $machine->waitUntilSucceeds("cat /root/.ssh/authorized_keys | grep -q 'should be a key!'");
+      machine.wait_until_succeeds(
+          "cat /root/.ssh/authorized_keys | grep -q 'should be a key!'"
+      )
   '';
 }
diff --git a/nixos/tests/colord.nix b/nixos/tests/colord.nix
deleted file mode 100644
index ce38aaca4bf..00000000000
--- a/nixos/tests/colord.nix
+++ /dev/null
@@ -1,18 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "colord";
-
-  meta = {
-    maintainers = pkgs.colord.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.colord.installedTests}/share'");
-  '';
-})
diff --git a/nixos/tests/couchdb.nix b/nixos/tests/couchdb.nix
index 48ea48eebbb..10e95701acd 100644
--- a/nixos/tests/couchdb.nix
+++ b/nixos/tests/couchdb.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ...}:
+import ./make-test-python.nix ({ pkgs, lib, ...}:
 
 with lib;
 
@@ -35,22 +35,42 @@ with lib;
         fi
       '';
   in ''
-    startAll;
-
-    $couchdb1->waitForUnit("couchdb.service");
-    $couchdb1->waitUntilSucceeds("${curlJqCheck "GET" "" ".couchdb" "Welcome"}");
-    $couchdb1->waitUntilSucceeds("${curlJqCheck "GET" "_all_dbs" ". | length" "2"}");
-    $couchdb1->succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}");
-    $couchdb1->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "3"}");
-    $couchdb1->succeed("${curlJqCheck "DELETE" "foo" ".ok" "true"}");
-    $couchdb1->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "2"}");
-
-    $couchdb2->waitForUnit("couchdb.service");
-    $couchdb2->waitUntilSucceeds("${curlJqCheck "GET" "" ".couchdb" "Welcome"}");
-    $couchdb2->waitUntilSucceeds("${curlJqCheck "GET" "_all_dbs" ". | length" "0"}");
-    $couchdb2->succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}");
-    $couchdb2->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "1"}");
-    $couchdb2->succeed("${curlJqCheck "DELETE" "foo" ".ok" "true"}");
-    $couchdb2->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "0"}");
+    start_all()
+
+    couchdb1.wait_for_unit("couchdb.service")
+    couchdb1.wait_until_succeeds(
+        "${curlJqCheck "GET" "" ".couchdb" "Welcome"}"
+    )
+    couchdb1.wait_until_succeeds(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "2"}"
+    )
+    couchdb1.succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}")
+    couchdb1.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "3"}"
+    )
+    couchdb1.succeed(
+        "${curlJqCheck "DELETE" "foo" ".ok" "true"}"
+    )
+    couchdb1.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "2"}"
+    )
+
+    couchdb2.wait_for_unit("couchdb.service")
+    couchdb2.wait_until_succeeds(
+        "${curlJqCheck "GET" "" ".couchdb" "Welcome"}"
+    )
+    couchdb2.wait_until_succeeds(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "0"}"
+    )
+    couchdb2.succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}")
+    couchdb2.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "1"}"
+    )
+    couchdb2.succeed(
+        "${curlJqCheck "DELETE" "foo" ".ok" "true"}"
+    )
+    couchdb2.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "0"}"
+    )
   '';
 })
diff --git a/nixos/tests/dnscrypt-proxy.nix b/nixos/tests/dnscrypt-proxy.nix
index 13bc9d3d916..98153d5c904 100644
--- a/nixos/tests/dnscrypt-proxy.nix
+++ b/nixos/tests/dnscrypt-proxy.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "dnscrypt-proxy";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ joachifm ];
@@ -23,11 +23,13 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    $client->waitForUnit("dnsmasq");
+    client.wait_for_unit("dnsmasq")
 
     # The daemon is socket activated; sending a single ping should activate it.
-    $client->fail("systemctl is-active dnscrypt-proxy");
-    $client->execute("${pkgs.iputils}/bin/ping -c1 example.com");
-    $client->waitUntilSucceeds("systemctl is-active dnscrypt-proxy");
+    client.fail("systemctl is-active dnscrypt-proxy")
+    client.execute(
+        "${pkgs.iputils}/bin/ping -c1 example.com"
+    )
+    client.wait_until_succeeds("systemctl is-active dnscrypt-proxy")
   '';
 })
diff --git a/nixos/tests/docker-edge.nix b/nixos/tests/docker-edge.nix
index b306c149be9..96de885a554 100644
--- a/nixos/tests/docker-edge.nix
+++ b/nixos/tests/docker-edge.nix
@@ -1,6 +1,6 @@
 # This test runs docker and checks if simple container starts
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus offline ];
@@ -31,17 +31,19 @@ import ./make-test.nix ({ pkgs, ...} : {
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $docker->waitForUnit("sockets.target");
-    $docker->succeed("tar cv --files-from /dev/null | docker import - scratchimg");
-    $docker->succeed("docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10");
-    $docker->succeed("docker ps | grep sleeping");
-    $docker->succeed("sudo -u hasprivs docker ps");
-    $docker->fail("sudo -u noprivs docker ps");
-    $docker->succeed("docker stop sleeping");
+    docker.wait_for_unit("sockets.target")
+    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    docker.succeed(
+        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    docker.succeed("docker ps | grep sleeping")
+    docker.succeed("sudo -u hasprivs docker ps")
+    docker.fail("sudo -u noprivs docker ps")
+    docker.succeed("docker stop sleeping")
 
     # Must match version twice to ensure client and server versions are correct
-    $docker->succeed('[ $(docker version | grep ${pkgs.docker-edge.version} | wc -l) = "2" ]');
+    docker.succeed('[ $(docker version | grep ${pkgs.docker-edge.version} | wc -l) = "2" ]')
   '';
 })
diff --git a/nixos/tests/docker.nix b/nixos/tests/docker.nix
index d67b2f8743d..8fda7c1395e 100644
--- a/nixos/tests/docker.nix
+++ b/nixos/tests/docker.nix
@@ -1,6 +1,6 @@
 # This test runs docker and checks if simple container starts
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus offline ];
@@ -31,17 +31,19 @@ import ./make-test.nix ({ pkgs, ...} : {
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $docker->waitForUnit("sockets.target");
-    $docker->succeed("tar cv --files-from /dev/null | docker import - scratchimg");
-    $docker->succeed("docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10");
-    $docker->succeed("docker ps | grep sleeping");
-    $docker->succeed("sudo -u hasprivs docker ps");
-    $docker->fail("sudo -u noprivs docker ps");
-    $docker->succeed("docker stop sleeping");
+    docker.wait_for_unit("sockets.target")
+    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    docker.succeed(
+        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    docker.succeed("docker ps | grep sleeping")
+    docker.succeed("sudo -u hasprivs docker ps")
+    docker.fail("sudo -u noprivs docker ps")
+    docker.succeed("docker stop sleeping")
 
     # Must match version twice to ensure client and server versions are correct
-    $docker->succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "2" ]');
+    docker.succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "2" ]')
   '';
 })
diff --git a/nixos/tests/documize.nix b/nixos/tests/documize.nix
index 8b852a4f779..3be20a780d3 100644
--- a/nixos/tests/documize.nix
+++ b/nixos/tests/documize.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ...} : {
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "documize";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ ma27 ];
@@ -29,30 +29,34 @@ import ./make-test.nix ({ pkgs, lib, ...} : {
   };
 
   testScript = ''
-    startAll;
-
-    $machine->waitForUnit("documize-server.service");
-    $machine->waitForOpenPort(3000);
-
-    my $dbhash = $machine->succeed("curl -f localhost:3000 "
-                                  . " | grep 'property=\"dbhash' "
-                                  . " | grep -Po 'content=\"\\K[^\"]*'"
-                                  );
-
-    chomp($dbhash);
-
-    $machine->succeed("curl -X POST "
-                      . "--data 'dbname=documize' "
-                      . "--data 'dbhash=$dbhash' "
-                      . "--data 'title=NixOS' "
-                      . "--data 'message=Docs' "
-                      . "--data 'firstname=John' "
-                      . "--data 'lastname=Doe' "
-                      . "--data 'email=john.doe\@nixos.org' "
-                      . "--data 'password=verysafe' "
-                      . "-f localhost:3000/api/setup"
-                    );
-
-    $machine->succeed('test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"');
+    start_all()
+
+    machine.wait_for_unit("documize-server.service")
+    machine.wait_for_open_port(3000)
+
+    dbhash = machine.succeed(
+        "curl -f localhost:3000 | grep 'property=\"dbhash' | grep -Po 'content=\"\\K[^\"]*'"
+    )
+
+    dbhash = dbhash.strip()
+
+    machine.succeed(
+        (
+            "curl -X POST"
+            " --data 'dbname=documize'"
+            " --data 'dbhash={}'"
+            " --data 'title=NixOS'"
+            " --data 'message=Docs'"
+            " --data 'firstname=John'"
+            " --data 'lastname=Doe'"
+            " --data 'email=john.doe@nixos.org'"
+            " --data 'password=verysafe'"
+            " -f localhost:3000/api/setup"
+        ).format(dbhash)
+    )
+
+    machine.succeed(
+        'test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"'
+    )
   '';
 })
diff --git a/nixos/tests/firefox.nix b/nixos/tests/firefox.nix
index f5b946a0881..56ddabbae77 100644
--- a/nixos/tests/firefox.nix
+++ b/nixos/tests/firefox.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "firefox";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ eelco shlevy ];
@@ -11,19 +11,27 @@ import ./make-test.nix ({ pkgs, ... }: {
       environment.systemPackages = [ pkgs.firefox pkgs.xdotool ];
     };
 
-  testScript =
-    ''
-      $machine->waitForX;
-      $machine->execute("xterm -e 'firefox file://${pkgs.valgrind.doc}/share/doc/valgrind/html/index.html' &");
-      $machine->waitForWindow(qr/Valgrind/);
-      $machine->sleep(40); # wait until Firefox has finished loading the page
-      $machine->execute("xdotool key space"); # do I want to make Firefox the
-                             # default browser? I just want to close the dialog
-      $machine->sleep(2); # wait until Firefox hides the default browser window
-      $machine->execute("xdotool key F12");
-      $machine->sleep(10); # wait until Firefox draws the developer tool panel
-      $machine->succeed("xwininfo -root -tree | grep Valgrind");
-      $machine->screenshot("screen");
+  testScript = ''
+      machine.wait_for_x()
+
+      with subtest("wait until Firefox has finished loading the Valgrind docs page"):
+          machine.execute(
+              "xterm -e 'firefox file://${pkgs.valgrind.doc}/share/doc/valgrind/html/index.html' &"
+          )
+          machine.wait_for_window("Valgrind")
+          machine.sleep(40)
+
+      with subtest("Close default browser prompt"):
+          machine.execute("xdotool key space")
+
+      with subtest("Hide default browser window"):
+          machine.sleep(2)
+          machine.execute("xdotool key F12")
+
+      with subtest("wait until Firefox draws the developer tool panel"):
+          machine.sleep(10)
+          machine.succeed("xwininfo -root -tree | grep Valgrind")
+          machine.screenshot("screen")
     '';
 
 })
diff --git a/nixos/tests/flatpak-builder.nix b/nixos/tests/flatpak-builder.nix
deleted file mode 100644
index 49b97e8ca99..00000000000
--- a/nixos/tests/flatpak-builder.nix
+++ /dev/null
@@ -1,20 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "flatpak-builder";
-  meta = {
-    maintainers = pkgs.flatpak-builder.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    services.flatpak.enable = true;
-    xdg.portal.enable = true;
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing flatpak-builder ] ++ flatpak-builder.installedTestsDependencies;
-    virtualisation.diskSize = 2048;
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.flatpak-builder.installedTests}/share' --timeout 3600");
-  '';
-})
diff --git a/nixos/tests/flatpak.nix b/nixos/tests/flatpak.nix
deleted file mode 100644
index b0c61830d05..00000000000
--- a/nixos/tests/flatpak.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "flatpak";
-  meta = {
-    maintainers = pkgs.flatpak.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    imports = [ ./common/x11.nix ];
-    services.xserver.desktopManager.gnome3.enable = true; # TODO: figure out minimal environment where the tests work
-    # common/x11.nix enables the auto display manager (lightdm)
-    services.xserver.displayManager.gdm.enable = false;
-    environment.gnome3.excludePackages = pkgs.gnome3.optionalPackages;
-    services.flatpak.enable = true;
-    environment.systemPackages = with pkgs; [ gnupg gnome-desktop-testing ostree python2 ];
-    virtualisation.memorySize = 2047;
-    virtualisation.diskSize = 1024;
-  };
-
-  testScript = ''
-    $machine->waitForX();
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.flatpak.installedTests}/share' --timeout 3600");
-  '';
-})
diff --git a/nixos/tests/fontconfig-default-fonts.nix b/nixos/tests/fontconfig-default-fonts.nix
index 1991cec9218..68c6ac9e9c8 100644
--- a/nixos/tests/fontconfig-default-fonts.nix
+++ b/nixos/tests/fontconfig-default-fonts.nix
@@ -1,7 +1,12 @@
-import ./make-test.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, ... }:
 {
   name = "fontconfig-default-fonts";
 
+  meta.maintainers = with lib.maintainers; [
+    jtojnar
+    worldofpeace
+  ];
+
   machine = { config, pkgs, ... }: {
     fonts.enableDefaultFonts = true; # Background fonts
     fonts.fonts = with pkgs; [
@@ -20,9 +25,9 @@ import ./make-test.nix ({ lib, ... }:
   };
 
   testScript = ''
-    $machine->succeed("fc-match serif | grep '\"Gentium Plus\"'");
-    $machine->succeed("fc-match sans-serif | grep '\"Cantarell\"'");
-    $machine->succeed("fc-match monospace | grep '\"Source Code Pro\"'");
-    $machine->succeed("fc-match emoji | grep '\"Twitter Color Emoji\"'");
+    machine.succeed("fc-match serif | grep '\"Gentium Plus\"'")
+    machine.succeed("fc-match sans-serif | grep '\"Cantarell\"'")
+    machine.succeed("fc-match monospace | grep '\"Source Code Pro\"'")
+    machine.succeed("fc-match emoji | grep '\"Twitter Color Emoji\"'")
   '';
 })
diff --git a/nixos/tests/fwupd.nix b/nixos/tests/fwupd.nix
deleted file mode 100644
index 88dac8ccbcd..00000000000
--- a/nixos/tests/fwupd.nix
+++ /dev/null
@@ -1,21 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }: {
-  name = "fwupd";
-
-  meta = {
-    maintainers = pkgs.fwupd.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    services.fwupd.enable = true;
-    services.fwupd.blacklistPlugins = []; # don't blacklist test plugin
-    services.fwupd.enableTestRemote = true;
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-    environment.variables.XDG_DATA_DIRS = [ "${pkgs.fwupd.installedTests}/share" ];
-    virtualisation.memorySize = 768;
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner");
-  '';
-})
diff --git a/nixos/tests/gdk-pixbuf.nix b/nixos/tests/gdk-pixbuf.nix
deleted file mode 100644
index 9a62b593f46..00000000000
--- a/nixos/tests/gdk-pixbuf.nix
+++ /dev/null
@@ -1,21 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }: {
-  name = "gdk-pixbuf";
-
-  meta = {
-    maintainers = pkgs.gdk-pixbuf.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-    environment.variables.XDG_DATA_DIRS = [ "${pkgs.gdk-pixbuf.installedTests}/share" ];
-
-    # Tests allocate a lot of memory trying to exploit a CVE
-    # but qemu-system-i386 has a 2047M memory limit
-    virtualisation.memorySize = if pkgs.stdenv.isi686 then 2047 else 4096;
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -t 1800"); # increase timeout to 1800s
-  '';
-})
diff --git a/nixos/tests/gjs.nix b/nixos/tests/gjs.nix
deleted file mode 100644
index 87c8d7f2817..00000000000
--- a/nixos/tests/gjs.nix
+++ /dev/null
@@ -1,19 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }: {
-  name = "gjs";
-
-  meta = {
-    maintainers = pkgs.gjs.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    imports = [ ./common/x11.nix ];
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-    environment.variables.XDG_DATA_DIRS = [ "${pkgs.gjs.installedTests}/share" ];
-  };
-
-  testScript = ''
-    $machine->waitForX;
-    $machine->succeed("gnome-desktop-testing-runner");
-  '';
-})
diff --git a/nixos/tests/glib-networking.nix b/nixos/tests/glib-networking.nix
deleted file mode 100644
index c0bbb2b3554..00000000000
--- a/nixos/tests/glib-networking.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "glib-networking";
-  meta = {
-    maintainers = pkgs.glib-networking.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.glib-networking.installedTests}/share'");
-  '';
-})
diff --git a/nixos/tests/gnome-photos.nix b/nixos/tests/gnome-photos.nix
deleted file mode 100644
index 2ecda1d68ce..00000000000
--- a/nixos/tests/gnome-photos.nix
+++ /dev/null
@@ -1,42 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, lib, ... }:
-
-let
-
-  # gsettings tool with access to gsettings-desktop-schemas
-  desktop-gsettings = with pkgs; stdenv.mkDerivation {
-    name = "desktop-gsettings";
-    dontUnpack = true;
-    nativeBuildInputs = [ glib wrapGAppsHook ];
-    buildInputs = [ gsettings-desktop-schemas ];
-    installPhase = ''
-      runHook preInstall
-      mkdir -p $out/bin
-      ln -s ${glib.bin}/bin/gsettings $out/bin/desktop-gsettings
-      runHook postInstall
-    '';
-  };
-
-in
-
-{
-  name = "gnome-photos";
-  meta = {
-    maintainers = pkgs.gnome-photos.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    imports = [ ./common/x11.nix ];
-    programs.dconf.enable = true;
-    services.gnome3.at-spi2-core.enable = true; # needed for dogtail
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing desktop-gsettings ];
-    services.dbus.packages = with pkgs; [ gnome-photos ];
-  };
-
-  testScript = ''
-    $machine->waitForX;
-    # dogtail needs accessibility enabled
-    $machine->succeed("desktop-gsettings set org.gnome.desktop.interface toolkit-accessibility true 2>&1");
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.gnome-photos.installedTests}/share' 2>&1");
-  '';
-})
diff --git a/nixos/tests/grafana.nix b/nixos/tests/grafana.nix
index 7a1b4c8ffbb..4b453ece7f1 100644
--- a/nixos/tests/grafana.nix
+++ b/nixos/tests/grafana.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, pkgs, ... }:
+import ./make-test-python.nix ({ lib, pkgs, ... }:
 
 let
   inherit (lib) mkMerge nameValuePair maintainers;
@@ -64,28 +64,34 @@ in {
   inherit nodes;
 
   testScript = ''
-    startAll();
+    start_all()
 
-    subtest "Grafana sqlite", sub {
-      $sqlite->waitForUnit("grafana.service");
-      $sqlite->waitForOpenPort(3000);
-      $sqlite->succeed("curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep -q testadmin\@localhost");
-    };
+    with subtest("Successful API query as admin user with sqlite db"):
+        sqlite.wait_for_unit("grafana.service")
+        sqlite.wait_for_open_port(3000)
+        sqlite.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep -q testadmin\@localhost"
+        )
+        sqlite.shutdown()
 
-    subtest "Grafana postgresql", sub {
-      $postgresql->waitForUnit("grafana.service");
-      $postgresql->waitForUnit("postgresql.service");
-      $postgresql->waitForOpenPort(3000);
-      $postgresql->waitForOpenPort(5432);
-      $postgresql->succeed("curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep -q testadmin\@localhost");
-    };
+    with subtest("Successful API query as admin user with postgresql db"):
+        postgresql.wait_for_unit("grafana.service")
+        postgresql.wait_for_unit("postgresql.service")
+        postgresql.wait_for_open_port(3000)
+        postgresql.wait_for_open_port(5432)
+        postgresql.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep -q testadmin\@localhost"
+        )
+        postgresql.shutdown()
 
-    subtest "Grafana mysql", sub {
-      $mysql->waitForUnit("grafana.service");
-      $mysql->waitForUnit("mysql.service");
-      $mysql->waitForOpenPort(3000);
-      $mysql->waitForOpenPort(3306);
-      $mysql->succeed("curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep -q testadmin\@localhost");
-    };
+    with subtest("Successful API query as admin user with mysql db"):
+        mysql.wait_for_unit("grafana.service")
+        mysql.wait_for_unit("mysql.service")
+        mysql.wait_for_open_port(3000)
+        mysql.wait_for_open_port(3306)
+        mysql.succeed(
+            "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/org/users | grep -q testadmin\@localhost"
+        )
+        mysql.shutdown()
   '';
 })
diff --git a/nixos/tests/graphene.nix b/nixos/tests/graphene.nix
deleted file mode 100644
index 5591bcc30c0..00000000000
--- a/nixos/tests/graphene.nix
+++ /dev/null
@@ -1,18 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "graphene";
-
-  meta = {
-    maintainers = pkgs.graphene.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.graphene.installedTests}/share'");
-  '';
-})
diff --git a/nixos/tests/initrd-network-ssh/default.nix b/nixos/tests/initrd-network-ssh/default.nix
index 796c50c610e..73d9f938e22 100644
--- a/nixos/tests/initrd-network-ssh/default.nix
+++ b/nixos/tests/initrd-network-ssh/default.nix
@@ -1,4 +1,4 @@
-import ../make-test.nix ({ lib, ... }:
+import ../make-test-python.nix ({ lib, ... }:
 
 {
   name = "initrd-network-ssh";
@@ -35,25 +35,31 @@ import ../make-test.nix ({ lib, ... }:
     client =
       { config, ... }:
       {
-        environment.etc.knownHosts = {
-          text = concatStrings [
-            "server,"
-            "${toString (head (splitString " " (
-              toString (elemAt (splitString "\n" config.networking.extraHosts) 2)
-            )))} "
-            "${readFile ./dropbear.pub}"
-          ];
+        environment.etc = {
+          knownHosts = {
+            text = concatStrings [
+              "server,"
+              "${toString (head (splitString " " (
+                toString (elemAt (splitString "\n" config.networking.extraHosts) 2)
+              )))} "
+              "${readFile ./dropbear.pub}"
+            ];
+          };
+          sshKey = {
+            source = ./openssh.priv; # dont use this anywhere else
+            mode = "0600";
+          };
         };
       };
   };
 
   testScript = ''
-    startAll;
-    $client->waitForUnit("network.target");
-    $client->copyFileFromHost("${./openssh.priv}","/etc/sshKey");
-    $client->succeed("chmod 0600 /etc/sshKey");
-    $client->waitUntilSucceeds("ping -c 1 server");
-    $client->succeed("ssh -i /etc/sshKey -o UserKnownHostsFile=/etc/knownHosts server 'touch /fnord'");
-    $client->shutdown;
+    start_all()
+    client.wait_for_unit("network.target")
+    client.wait_until_succeeds("ping -c 1 server")
+    client.succeed(
+        "ssh -i /etc/sshKey -o UserKnownHostsFile=/etc/knownHosts server 'touch /fnord'"
+    )
+    client.shutdown()
   '';
 })
diff --git a/nixos/tests/installed-tests/colord.nix b/nixos/tests/installed-tests/colord.nix
new file mode 100644
index 00000000000..77e6b917fe6
--- /dev/null
+++ b/nixos/tests/installed-tests/colord.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.colord;
+}
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
new file mode 100644
index 00000000000..f4780bdcfc9
--- /dev/null
+++ b/nixos/tests/installed-tests/default.nix
@@ -0,0 +1,80 @@
+# NixOS tests for gnome-desktop-testing-runner using software
+# See https://wiki.gnome.org/Initiatives/GnomeGoals/InstalledTests
+
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../../.. { inherit system config; }
+}:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+
+  callInstalledTest = pkgs.newScope { inherit makeInstalledTest; };
+
+  makeInstalledTest =
+    { # Package to test. Needs to have an installedTests output
+      tested
+
+      # Config to inject into machine
+    , testConfig ? {}
+
+      # Test script snippet to inject before gnome-desktop-testing-runner begins.
+      # This is useful for extra setup the environment may need before the runner begins.
+    , preTestScript ? ""
+
+      # Does test need X11?
+    , withX11 ? false
+
+      # Extra flags to pass to gnome-desktop-testing-runner.
+    , testRunnerFlags ? ""
+    }:
+    makeTest rec {
+      name = tested.name;
+
+      meta = {
+        maintainers = tested.meta.maintainers;
+      };
+
+      machine = { ... }: {
+        imports = [
+          testConfig
+        ] ++ optional withX11 ../common/x11.nix;
+
+        environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
+
+      };
+
+      testScript =
+        optionalString withX11 ''
+          machine.wait_for_x()
+        '' +
+        optionalString (preTestScript != "") ''
+          ${preTestScript}
+        '' +
+        ''
+          machine.succeed(
+              "gnome-desktop-testing-runner ${testRunnerFlags} -d '${tested.installedTests}/share'"
+          )
+        '';
+    };
+
+in
+
+{
+  colord = callInstalledTest ./colord.nix {};
+  flatpak = callInstalledTest ./flatpak.nix {};
+  flatpak-builder = callInstalledTest ./flatpak-builder.nix {};
+  fwupd = callInstalledTest ./fwupd.nix {};
+  gcab = callInstalledTest ./gcab.nix {};
+  gdk-pixbuf = callInstalledTest ./gdk-pixbuf.nix {};
+  gjs = callInstalledTest ./gjs.nix {};
+  glib-networking = callInstalledTest ./glib-networking.nix {};
+  gnome-photos = callInstalledTest ./gnome-photos.nix {};
+  graphene = callInstalledTest ./graphene.nix {};
+  libgdata = callInstalledTest ./libgdata.nix {};
+  libxmlb = callInstalledTest ./libxmlb.nix {};
+  ostree = callInstalledTest ./ostree.nix {};
+  xdg-desktop-portal = callInstalledTest ./xdg-desktop-portal.nix {};
+}
diff --git a/nixos/tests/installed-tests/flatpak-builder.nix b/nixos/tests/installed-tests/flatpak-builder.nix
new file mode 100644
index 00000000000..31b9f2b258f
--- /dev/null
+++ b/nixos/tests/installed-tests/flatpak-builder.nix
@@ -0,0 +1,14 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.flatpak-builder;
+
+  testConfig = {
+    services.flatpak.enable = true;
+    xdg.portal.enable = true;
+    environment.systemPackages = with pkgs; [ flatpak-builder ] ++ flatpak-builder.installedTestsDependencies;
+    virtualisation.diskSize = 2048;
+  };
+
+  testRunnerFlags = "--timeout 3600";
+}
diff --git a/nixos/tests/installed-tests/flatpak.nix b/nixos/tests/installed-tests/flatpak.nix
new file mode 100644
index 00000000000..091c9932662
--- /dev/null
+++ b/nixos/tests/installed-tests/flatpak.nix
@@ -0,0 +1,19 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.flatpak;
+  withX11 = true;
+
+  testConfig = {
+    services.xserver.desktopManager.gnome3.enable = true; # TODO: figure out minimal environment where the tests work
+    # common/x11.nix enables the auto display manager (lightdm)
+    services.xserver.displayManager.gdm.enable = false;
+    services.gnome3.core-utilities.enable = false;
+    services.flatpak.enable = true;
+    environment.systemPackages = with pkgs; [ gnupg ostree python2 ];
+    virtualisation.memorySize = 2047;
+    virtualisation.diskSize = 1024;
+  };
+
+  testRunnerFlags = "--timeout 3600";
+}
diff --git a/nixos/tests/installed-tests/fwupd.nix b/nixos/tests/installed-tests/fwupd.nix
new file mode 100644
index 00000000000..b9f761e9958
--- /dev/null
+++ b/nixos/tests/installed-tests/fwupd.nix
@@ -0,0 +1,12 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.fwupd;
+
+  testConfig = {
+    services.fwupd.enable = true;
+    services.fwupd.blacklistPlugins = []; # don't blacklist test plugin
+    services.fwupd.enableTestRemote = true;
+    virtualisation.memorySize = 768;
+  };
+}
diff --git a/nixos/tests/installed-tests/gcab.nix b/nixos/tests/installed-tests/gcab.nix
new file mode 100644
index 00000000000..b24cc2e0126
--- /dev/null
+++ b/nixos/tests/installed-tests/gcab.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gcab;
+}
diff --git a/nixos/tests/installed-tests/gdk-pixbuf.nix b/nixos/tests/installed-tests/gdk-pixbuf.nix
new file mode 100644
index 00000000000..3d0011a427a
--- /dev/null
+++ b/nixos/tests/installed-tests/gdk-pixbuf.nix
@@ -0,0 +1,13 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gdk-pixbuf;
+
+  testConfig = {
+    # Tests allocate a lot of memory trying to exploit a CVE
+    # but qemu-system-i386 has a 2047M memory limit
+    virtualisation.memorySize = if pkgs.stdenv.isi686 then 2047 else 4096;
+  };
+
+  testRunnerFlags = "--timeout 1800";
+}
diff --git a/nixos/tests/installed-tests/gjs.nix b/nixos/tests/installed-tests/gjs.nix
new file mode 100644
index 00000000000..1656e9de171
--- /dev/null
+++ b/nixos/tests/installed-tests/gjs.nix
@@ -0,0 +1,6 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gjs;
+  withX11 = true;
+}
diff --git a/nixos/tests/installed-tests/glib-networking.nix b/nixos/tests/installed-tests/glib-networking.nix
new file mode 100644
index 00000000000..b58d4df21fc
--- /dev/null
+++ b/nixos/tests/installed-tests/glib-networking.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.glib-networking;
+}
diff --git a/nixos/tests/installed-tests/gnome-photos.nix b/nixos/tests/installed-tests/gnome-photos.nix
new file mode 100644
index 00000000000..05e7ccb65ad
--- /dev/null
+++ b/nixos/tests/installed-tests/gnome-photos.nix
@@ -0,0 +1,35 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.gnome-photos;
+
+  withX11 = true;
+
+  testConfig = {
+    programs.dconf.enable = true;
+    services.gnome3.at-spi2-core.enable = true; # needed for dogtail
+    environment.systemPackages = with pkgs; [
+      # gsettings tool with access to gsettings-desktop-schemas
+      (stdenv.mkDerivation {
+        name = "desktop-gsettings";
+        dontUnpack = true;
+        nativeBuildInputs = [ glib wrapGAppsHook ];
+        buildInputs = [ gsettings-desktop-schemas ];
+        installPhase = ''
+          runHook preInstall
+          mkdir -p $out/bin
+          ln -s ${glib.bin}/bin/gsettings $out/bin/desktop-gsettings
+          runHook postInstall
+        '';
+      })
+    ];
+    services.dbus.packages = with pkgs; [ gnome-photos ];
+  };
+
+  preTestScript = ''
+    # dogtail needs accessibility enabled
+    machine.succeed(
+        "desktop-gsettings set org.gnome.desktop.interface toolkit-accessibility true 2>&1"
+    )
+  '';
+}
diff --git a/nixos/tests/installed-tests/graphene.nix b/nixos/tests/installed-tests/graphene.nix
new file mode 100644
index 00000000000..e43339abd88
--- /dev/null
+++ b/nixos/tests/installed-tests/graphene.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.graphene;
+}
diff --git a/nixos/tests/installed-tests/libgdata.nix b/nixos/tests/installed-tests/libgdata.nix
new file mode 100644
index 00000000000..f11a7bc1bc5
--- /dev/null
+++ b/nixos/tests/installed-tests/libgdata.nix
@@ -0,0 +1,11 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libgdata;
+
+  testConfig = {
+    # # GLib-GIO-DEBUG: _g_io_module_get_default: Found default implementation dummy (GDummyTlsBackend) for ‘gio-tls-backend’
+    # Bail out! libgdata:ERROR:../gdata/tests/common.c:134:gdata_test_init: assertion failed (child_error == NULL): TLS support is not available (g-tls-error-quark, 0)
+    services.gnome3.glib-networking.enable = true;
+  };
+}
diff --git a/nixos/tests/installed-tests/libxmlb.nix b/nixos/tests/installed-tests/libxmlb.nix
new file mode 100644
index 00000000000..af2bbe9c35e
--- /dev/null
+++ b/nixos/tests/installed-tests/libxmlb.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.libxmlb;
+}
diff --git a/nixos/tests/installed-tests/ostree.nix b/nixos/tests/installed-tests/ostree.nix
new file mode 100644
index 00000000000..eef7cace54c
--- /dev/null
+++ b/nixos/tests/installed-tests/ostree.nix
@@ -0,0 +1,23 @@
+{ pkgs, lib, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.ostree;
+
+  # TODO: Wrap/patch the tests directly in the package
+  testConfig = {
+    environment.systemPackages = with pkgs; [
+      (python3.withPackages (p: with p; [ pyyaml ]))
+      gnupg
+      ostree
+    ];
+
+    # for GJS tests
+    environment.variables.GI_TYPELIB_PATH = lib.makeSearchPath "lib/girepository-1.0" (with pkgs; [
+      gtk3
+      pango.out
+      ostree
+      gdk-pixbuf
+      atk
+    ]);
+  };
+}
diff --git a/nixos/tests/installed-tests/xdg-desktop-portal.nix b/nixos/tests/installed-tests/xdg-desktop-portal.nix
new file mode 100644
index 00000000000..b16008ff4ad
--- /dev/null
+++ b/nixos/tests/installed-tests/xdg-desktop-portal.nix
@@ -0,0 +1,5 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.xdg-desktop-portal;
+}
diff --git a/nixos/tests/libgdata.nix b/nixos/tests/libgdata.nix
deleted file mode 100644
index 10a3ca97dd2..00000000000
--- a/nixos/tests/libgdata.nix
+++ /dev/null
@@ -1,21 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "libgdata";
-
-  meta = {
-    maintainers = pkgs.libgdata.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-    # # GLib-GIO-DEBUG: _g_io_module_get_default: Found default implementation dummy (GDummyTlsBackend) for ‘gio-tls-backend’
-    # Bail out! libgdata:ERROR:../gdata/tests/common.c:134:gdata_test_init: assertion failed (child_error == NULL): TLS support is not available (g-tls-error-quark, 0)
-    services.gnome3.glib-networking.enable = true;
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.libgdata.installedTests}/share'");
-  '';
-})
diff --git a/nixos/tests/libxmlb.nix b/nixos/tests/libxmlb.nix
deleted file mode 100644
index 3bee568ac5a..00000000000
--- a/nixos/tests/libxmlb.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "libxmlb";
-  meta = {
-    maintainers = pkgs.libxmlb.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.libxmlb.installedTests}/share'");
-  '';
-})
diff --git a/nixos/tests/loki.nix b/nixos/tests/loki.nix
index 9c3058d02f8..dbf1e8a650f 100644
--- a/nixos/tests/loki.nix
+++ b/nixos/tests/loki.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, pkgs, ... }:
+import ./make-test-python.nix ({ lib, pkgs, ... }:
 
 {
   name = "loki";
@@ -26,12 +26,14 @@ import ./make-test.nix ({ lib, pkgs, ... }:
   };
 
   testScript = ''
-    $machine->start;
-    $machine->waitForUnit("loki.service");
-    $machine->waitForUnit("promtail.service");
-    $machine->waitForOpenPort(3100);
-    $machine->waitForOpenPort(9080);
-    $machine->succeed("echo 'Loki Ingestion Test' > /var/log/testlog");
-    $machine->waitUntilSucceeds("${pkgs.grafana-loki}/bin/logcli --addr='http://localhost:3100' query --no-labels '{job=\"varlogs\",filename=\"/var/log/testlog\"}' | grep -q 'Loki Ingestion Test'");
+    machine.start
+    machine.wait_for_unit("loki.service")
+    machine.wait_for_unit("promtail.service")
+    machine.wait_for_open_port(3100)
+    machine.wait_for_open_port(9080)
+    machine.succeed("echo 'Loki Ingestion Test' > /var/log/testlog")
+    machine.wait_until_succeeds(
+        "${pkgs.grafana-loki}/bin/logcli --addr='http://localhost:3100' query --no-labels '{job=\"varlogs\",filename=\"/var/log/testlog\"}' | grep -q 'Loki Ingestion Test'"
+    )
   '';
 })
diff --git a/nixos/tests/matrix-synapse.nix b/nixos/tests/matrix-synapse.nix
index 882e4b75814..fca53009083 100644
--- a/nixos/tests/matrix-synapse.nix
+++ b/nixos/tests/matrix-synapse.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... } : let
+import ./make-test-python.nix ({ pkgs, ... } : let
 
 
   runWithOpenSSL = file: cmd: pkgs.runCommand file {
@@ -55,13 +55,17 @@ in {
   };
 
   testScript = ''
-    startAll;
-    $serverpostgres->waitForUnit("matrix-synapse.service");
-    $serverpostgres->waitUntilSucceeds("curl -L --cacert ${ca_pem} https://localhost:8448/");
-    $serverpostgres->requireActiveUnit("postgresql.service");
-    $serversqlite->waitForUnit("matrix-synapse.service");
-    $serversqlite->waitUntilSucceeds("curl -L --cacert ${ca_pem} https://localhost:8448/");
-    $serversqlite->mustSucceed("[ -e /var/lib/matrix-synapse/homeserver.db ]");
+    start_all()
+    serverpostgres.wait_for_unit("matrix-synapse.service")
+    serverpostgres.wait_until_succeeds(
+        "curl -L --cacert ${ca_pem} https://localhost:8448/"
+    )
+    serverpostgres.require_unit_state("postgresql.service")
+    serversqlite.wait_for_unit("matrix-synapse.service")
+    serversqlite.wait_until_succeeds(
+        "curl -L --cacert ${ca_pem} https://localhost:8448/"
+    )
+    serversqlite.succeed("[ -e /var/lib/matrix-synapse/homeserver.db ]")
   '';
 
 })
diff --git a/nixos/tests/moodle.nix b/nixos/tests/moodle.nix
index 565a6b63694..56aa62596c0 100644
--- a/nixos/tests/moodle.nix
+++ b/nixos/tests/moodle.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ... }: {
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "moodle";
   meta.maintainers = [ lib.maintainers.aanderse ];
 
@@ -15,8 +15,8 @@ import ./make-test.nix ({ pkgs, lib, ... }: {
     };
 
   testScript = ''
-    startAll;
-    $machine->waitForUnit('phpfpm-moodle.service');
-    $machine->succeed('curl http://localhost/') =~ /You are not logged in/ or die;
+    start_all()
+    machine.wait_for_unit("phpfpm-moodle.service")
+    machine.wait_until_succeeds("curl http://localhost/ | grep 'You are not logged in'")
   '';
 })
diff --git a/nixos/tests/morty.nix b/nixos/tests/morty.nix
index eab123bd50f..64c5a27665d 100644
--- a/nixos/tests/morty.nix
+++ b/nixos/tests/morty.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "morty";
@@ -22,11 +22,9 @@ import ./make-test.nix ({ pkgs, ... }:
   testScript =
     { ... }:
     ''
-      $mortyProxyWithKey->waitForUnit("default.target");
-
-      $mortyProxyWithKey->waitForOpenPort(3001);
-      $mortyProxyWithKey->succeed("curl -L 127.0.0.1:3001 | grep MortyProxy");
-
+      mortyProxyWithKey.wait_for_unit("default.target")
+      mortyProxyWithKey.wait_for_open_port(3001)
+      mortyProxyWithKey.succeed("curl -L 127.0.0.1:3001 | grep MortyProxy")
     '';
 
 })
diff --git a/nixos/tests/ostree.nix b/nixos/tests/ostree.nix
deleted file mode 100644
index d7ad84a1a5f..00000000000
--- a/nixos/tests/ostree.nix
+++ /dev/null
@@ -1,21 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, lib, ... }: {
-  name = "ostree";
-
-  meta = {
-    maintainers = pkgs.ostree.meta.maintainers;
-  };
-
-  # TODO: Wrap/patch the tests directly in the package
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [
-      gnome-desktop-testing ostree gnupg (python3.withPackages (p: with p; [ pyyaml ]))
-    ];
-
-    environment.variables.GI_TYPELIB_PATH = lib.makeSearchPath "lib/girepository-1.0" (with pkgs; [ gtk3 pango.out ostree gdk-pixbuf atk ]); # for GJS tests
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d ${pkgs.ostree.installedTests}/share");
-  '';
-})
diff --git a/nixos/tests/packagekit.nix b/nixos/tests/packagekit.nix
index e2d68af661f..7e93ad35e80 100644
--- a/nixos/tests/packagekit.nix
+++ b/nixos/tests/packagekit.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "packagekit";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ peterhoeg ];
@@ -13,12 +13,14 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
     # send a dbus message to activate the service
-    $machine->succeed("dbus-send --system --type=method_call --print-reply --dest=org.freedesktop.PackageKit /org/freedesktop/PackageKit org.freedesktop.DBus.Introspectable.Introspect");
+    machine.succeed(
+        "dbus-send --system --type=method_call --print-reply --dest=org.freedesktop.PackageKit /org/freedesktop/PackageKit org.freedesktop.DBus.Introspectable.Introspect"
+    )
 
     # so now it should be running
-    $machine->succeed("systemctl is-active packagekit.service");
+    machine.wait_for_unit("packagekit.service")
   '';
 })
diff --git a/nixos/tests/pgjwt.nix b/nixos/tests/pgjwt.nix
index a2d81288c81..4793a3e3150 100644
--- a/nixos/tests/pgjwt.nix
+++ b/nixos/tests/pgjwt.nix
@@ -1,12 +1,5 @@
-import ./make-test.nix ({ pkgs, lib, ...}:
-let
-  test = with pkgs; runCommand "patch-test" {
-    nativeBuildInputs = [ pgjwt ];
-  }
-  ''
-    sed -e '12 i CREATE EXTENSION pgcrypto;\nCREATE EXTENSION pgtap;\nSET search_path TO tap,public;' ${pgjwt.src}/test.sql > $out;
-  '';
-in
+import ./make-test-python.nix ({ pkgs, lib, ...}:
+
 with pkgs; {
   name = "pgjwt";
   meta = with lib.maintainers; {
@@ -29,9 +22,13 @@ with pkgs; {
     pgProve = "${pkgs.perlPackages.TAPParserSourceHandlerpgTAP}";
   in
   ''
-    startAll;
-    $master->waitForUnit("postgresql");
-    $master->copyFileFromHost("${test}","/tmp/test.sql");
-    $master->succeed("${pkgs.sudo}/bin/sudo -u ${sqlSU} PGOPTIONS=--search_path=tap,public ${pgProve}/bin/pg_prove -d postgres -v -f /tmp/test.sql");
+    start_all()
+    master.wait_for_unit("postgresql")
+    master.succeed(
+        "${pkgs.gnused}/bin/sed -e '12 i CREATE EXTENSION pgcrypto;\\nCREATE EXTENSION pgtap;\\nSET search_path TO tap,public;' ${pgjwt.src}/test.sql > /tmp/test.sql"
+    )
+    master.succeed(
+        "${pkgs.sudo}/bin/sudo -u ${sqlSU} PGOPTIONS=--search_path=tap,public ${pgProve}/bin/pg_prove -d postgres -v -f /tmp/test.sql"
+    )
   '';
 })
diff --git a/nixos/tests/powerdns.nix b/nixos/tests/powerdns.nix
index 8addcc78401..75d71315e64 100644
--- a/nixos/tests/powerdns.nix
+++ b/nixos/tests/powerdns.nix
@@ -1,12 +1,13 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "powerdns";
 
   nodes.server = { ... }: {
     services.powerdns.enable = true;
+    environment.systemPackages = [ pkgs.dnsutils ];
   };
 
   testScript = ''
-    $server->waitForUnit("pdns");
-    $server->succeed("${pkgs.dnsutils}/bin/dig version.bind txt chaos \@127.0.0.1");
+    server.wait_for_unit("pdns")
+    server.succeed("dig version.bind txt chaos \@127.0.0.1")
   '';
 })
diff --git a/nixos/tests/pppd.nix b/nixos/tests/pppd.nix
index 91f81185909..bda0aa75bb5 100644
--- a/nixos/tests/pppd.nix
+++ b/nixos/tests/pppd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix (
+import ./make-test-python.nix (
   let
     chap-secrets = {
       text = ''"flynn" * "reindeerflotilla" *'';
@@ -53,10 +53,10 @@ import ./make-test.nix (
         environment.etc."ppp/chap-secrets" = chap-secrets;
       };
     };
-  
+
     testScript = ''
-      startAll;
-      $client->waitUntilSucceeds("ping -c1 -W1 192.0.2.1");
-      $server->waitUntilSucceeds("ping -c1 -W1 192.0.2.2");
+      start_all()
+      client.wait_until_succeeds("ping -c1 -W1 192.0.2.1")
+      server.wait_until_succeeds("ping -c1 -W1 192.0.2.2")
     '';
-  })  
+  })
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 676183f6356..76cecb7433a 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -4,12 +4,10 @@
 }:
 
 let
-  inherit (import ../lib/testing.nix { inherit system pkgs; }) makeTest;
+  inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
   inherit (pkgs.lib) concatStringsSep maintainers mapAttrs mkMerge
                      removeSuffix replaceChars singleton splitString;
 
-  escape' = str: replaceChars [''"'' "$" "\n"] [''\\\"'' "\\$" ""] str;
-
 /*
  * The attrset `exporterTests` contains one attribute
  * for each exporter test. Each of these attributes
@@ -33,9 +31,9 @@ let
  *        services.<metricProvider>.enable = true;
  *      };
  *      exporterTest = ''
- *        waitForUnit("prometheus-<exporterName>-exporter.service");
- *        waitForOpenPort("1234");
- *        succeed("curl -sSf 'localhost:1234/metrics'");
+ *        wait_for_unit("prometheus-<exporterName>-exporter.service")
+ *        wait_for_open_port("1234")
+ *        succeed("curl -sSf 'localhost:1234/metrics'")
  *      '';
  *    };
  *
@@ -49,11 +47,11 @@ let
  *    };
  *
  *    testScript = ''
- *      $<exporterName>->start();
- *      $<exporterName>->waitForUnit("prometheus-<exporterName>-exporter.service");
- *      $<exporterName>->waitForOpenPort("1234");
- *      $<exporterName>->succeed("curl -sSf 'localhost:1234/metrics'");
- *      $<exporterName>->shutdown();
+ *      <exporterName>.start()
+ *      <exporterName>.wait_for_unit("prometheus-<exporterName>-exporter.service")
+ *      <exporterName>.wait_for_open_port("1234")
+ *      <exporterName>.succeed("curl -sSf 'localhost:1234/metrics'")
+ *      <exporterName>.shutdown()
  *    '';
  */
 
@@ -72,9 +70,11 @@ let
         '';
       };
       exporterTest = ''
-        waitForUnit("prometheus-bind-exporter.service");
-        waitForOpenPort(9119);
-        succeed("curl -sSf http://localhost:9119/metrics | grep -q 'bind_query_recursions_total 0'");
+        wait_for_unit("prometheus-bind-exporter.service")
+        wait_for_open_port(9119)
+        succeed(
+            "curl -sSf http://localhost:9119/metrics | grep -q 'bind_query_recursions_total 0'"
+        )
       '';
     };
 
@@ -89,9 +89,11 @@ let
         });
       };
       exporterTest = ''
-        waitForUnit("prometheus-blackbox-exporter.service");
-        waitForOpenPort(9115);
-        succeed("curl -sSf 'http://localhost:9115/probe?target=localhost&module=icmp_v6' | grep -q 'probe_success 1'");
+        wait_for_unit("prometheus-blackbox-exporter.service")
+        wait_for_open_port(9115)
+        succeed(
+            "curl -sSf 'http://localhost:9115/probe?target=localhost&module=icmp_v6' | grep -q 'probe_success 1'"
+        )
       '';
     };
 
@@ -100,7 +102,7 @@ let
         enable = true;
         extraFlags = [ "--web.collectd-push-path /collectd" ];
       };
-      exporterTest =let postData = escape' ''
+      exporterTest = let postData = replaceChars [ "\n" ] [ "" ] ''
         [{
           "values":[23],
           "dstypes":["gauge"],
@@ -108,13 +110,21 @@ let
           "interval":1000,
           "host":"testhost",
           "plugin":"testplugin",
-          "time":$(date +%s)
+          "time":DATE
         }]
         ''; in ''
-        waitForUnit("prometheus-collectd-exporter.service");
-        waitForOpenPort(9103);
-        succeed("curl -sSfH 'Content-Type: application/json' -X POST --data \"${postData}\" localhost:9103/collectd");
-        succeed("curl -sSf localhost:9103/metrics | grep -q 'collectd_testplugin_gauge{instance=\"testhost\"} 23'");
+        wait_for_unit("prometheus-collectd-exporter.service")
+        wait_for_open_port(9103)
+        succeed(
+            'echo \'${postData}\'> /tmp/data.json'
+        )
+        succeed('sed -ie "s DATE $(date +%s) " /tmp/data.json')
+        succeed(
+            "curl -sSfH 'Content-Type: application/json' -X POST --data @/tmp/data.json localhost:9103/collectd"
+        )
+        succeed(
+            "curl -sSf localhost:9103/metrics | grep -q 'collectd_testplugin_gauge{instance=\"testhost\"} 23'"
+        )
       '';
     };
 
@@ -127,9 +137,9 @@ let
         services.dnsmasq.enable = true;
       };
       exporterTest = ''
-        waitForUnit("prometheus-dnsmasq-exporter.service");
-        waitForOpenPort(9153);
-        succeed("curl -sSf http://localhost:9153/metrics | grep -q 'dnsmasq_leases 0'");
+        wait_for_unit("prometheus-dnsmasq-exporter.service")
+        wait_for_open_port(9153)
+        succeed("curl -sSf http://localhost:9153/metrics | grep -q 'dnsmasq_leases 0'")
       '';
     };
 
@@ -144,9 +154,11 @@ let
         services.dovecot2.enable = true;
       };
       exporterTest = ''
-        waitForUnit("prometheus-dovecot-exporter.service");
-        waitForOpenPort(9166);
-        succeed("curl -sSf http://localhost:9166/metrics | grep -q 'dovecot_up{scope=\"global\"} 1'");
+        wait_for_unit("prometheus-dovecot-exporter.service")
+        wait_for_open_port(9166)
+        succeed(
+            "curl -sSf http://localhost:9166/metrics | grep -q 'dovecot_up{scope=\"global\"} 1'"
+        )
       '';
     };
 
@@ -155,9 +167,11 @@ let
         enable = true;
       };
       exporterTest = ''
-        waitForUnit("prometheus-fritzbox-exporter.service");
-        waitForOpenPort(9133);
-        succeed("curl -sSf http://localhost:9133/metrics | grep -q 'fritzbox_exporter_collect_errors 0'");
+        wait_for_unit("prometheus-fritzbox-exporter.service")
+        wait_for_open_port(9133)
+        succeed(
+            "curl -sSf http://localhost:9133/metrics | grep -q 'fritzbox_exporter_collect_errors 0'"
+        )
       '';
     };
 
@@ -180,11 +194,11 @@ let
         };
       };
       exporterTest = ''
-        waitForUnit("nginx.service");
-        waitForOpenPort(80);
-        waitForUnit("prometheus-json-exporter.service");
-        waitForOpenPort(7979);
-        succeed("curl -sSf localhost:7979/metrics | grep -q 'json_test_metric 1'");
+        wait_for_unit("nginx.service")
+        wait_for_open_port(80)
+        wait_for_unit("prometheus-json-exporter.service")
+        wait_for_open_port(7979)
+        succeed("curl -sSf localhost:7979/metrics | grep -q 'json_test_metric 1'")
       '';
     };
 
@@ -222,10 +236,12 @@ let
         users.users.mailexporter.isSystemUser = true;
       };
       exporterTest = ''
-        waitForUnit("postfix.service")
-        waitForUnit("prometheus-mail-exporter.service")
-        waitForOpenPort(9225)
-        waitUntilSucceeds("curl -sSf http://localhost:9225/metrics | grep -q 'mail_deliver_success{configname=\"testserver\"} 1'")
+        wait_for_unit("postfix.service")
+        wait_for_unit("prometheus-mail-exporter.service")
+        wait_for_open_port(9225)
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9225/metrics | grep -q 'mail_deliver_success{configname=\"testserver\"} 1'"
+        )
       '';
     };
 
@@ -256,9 +272,9 @@ let
         };
       };
       exporterTest = ''
-        waitForUnit("nginx.service")
-        waitForUnit("prometheus-nextcloud-exporter.service")
-        waitForOpenPort(9205)
+        wait_for_unit("nginx.service")
+        wait_for_unit("prometheus-nextcloud-exporter.service")
+        wait_for_open_port(9205)
         succeed("curl -sSf http://localhost:9205/metrics | grep -q 'nextcloud_up 1'")
       '';
     };
@@ -275,9 +291,9 @@ let
         };
       };
       exporterTest = ''
-        waitForUnit("nginx.service")
-        waitForUnit("prometheus-nginx-exporter.service")
-        waitForOpenPort(9113)
+        wait_for_unit("nginx.service")
+        wait_for_unit("prometheus-nginx-exporter.service")
+        wait_for_open_port(9113)
         succeed("curl -sSf http://localhost:9113/metrics | grep -q 'nginx_up 1'")
       '';
     };
@@ -287,9 +303,11 @@ let
         enable = true;
       };
       exporterTest = ''
-        waitForUnit("prometheus-node-exporter.service");
-        waitForOpenPort(9100);
-        succeed("curl -sSf http://localhost:9100/metrics | grep -q 'node_exporter_build_info{.\\+} 1'");
+        wait_for_unit("prometheus-node-exporter.service")
+        wait_for_open_port(9100)
+        succeed(
+            "curl -sSf http://localhost:9100/metrics | grep -q 'node_exporter_build_info{.\\+} 1'"
+        )
       '';
     };
 
@@ -301,9 +319,11 @@ let
         services.postfix.enable = true;
       };
       exporterTest = ''
-        waitForUnit("prometheus-postfix-exporter.service");
-        waitForOpenPort(9154);
-        succeed("curl -sSf http://localhost:9154/metrics | grep -q 'postfix_smtpd_connects_total 0'");
+        wait_for_unit("prometheus-postfix-exporter.service")
+        wait_for_open_port(9154)
+        succeed(
+            "curl -sSf http://localhost:9154/metrics | grep -q 'postfix_smtpd_connects_total 0'"
+        )
       '';
     };
 
@@ -316,18 +336,24 @@ let
         services.postgresql.enable = true;
       };
       exporterTest = ''
-        waitForUnit("prometheus-postgres-exporter.service");
-        waitForOpenPort(9187);
-        waitForUnit("postgresql.service");
-        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_exporter_last_scrape_error 0'");
-        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_up 1'");
-        systemctl("stop postgresql.service");
-        succeed("curl -sSf http://localhost:9187/metrics | grep -qv 'pg_exporter_last_scrape_error 0'");
-        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_up 0'");
-        systemctl("start postgresql.service");
-        waitForUnit("postgresql.service");
-        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_exporter_last_scrape_error 0'");
-        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_up 1'");
+        wait_for_unit("prometheus-postgres-exporter.service")
+        wait_for_open_port(9187)
+        wait_for_unit("postgresql.service")
+        succeed(
+            "curl -sSf http://localhost:9187/metrics | grep -q 'pg_exporter_last_scrape_error 0'"
+        )
+        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_up 1'")
+        systemctl("stop postgresql.service")
+        succeed(
+            "curl -sSf http://localhost:9187/metrics | grep -qv 'pg_exporter_last_scrape_error 0'"
+        )
+        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_up 0'")
+        systemctl("start postgresql.service")
+        wait_for_unit("postgresql.service")
+        succeed(
+            "curl -sSf http://localhost:9187/metrics | grep -q 'pg_exporter_last_scrape_error 0'"
+        )
+        succeed("curl -sSf http://localhost:9187/metrics | grep -q 'pg_up 1'")
       '';
     };
 
@@ -339,11 +365,13 @@ let
         services.rspamd.enable = true;
       };
       exporterTest = ''
-        waitForUnit("rspamd.service");
-        waitForUnit("prometheus-rspamd-exporter.service");
-        waitForOpenPort(11334);
-        waitForOpenPort(7980);
-        waitUntilSucceeds("curl -sSf localhost:7980/metrics | grep -q 'rspamd_scanned{host=\"rspamd\"} 0'");
+        wait_for_unit("rspamd.service")
+        wait_for_unit("prometheus-rspamd-exporter.service")
+        wait_for_open_port(11334)
+        wait_for_open_port(7980)
+        wait_until_succeeds(
+            "curl -sSf localhost:7980/metrics | grep -q 'rspamd_scanned{host=\"rspamd\"} 0'"
+        )
       '';
     };
 
@@ -356,9 +384,9 @@ let
         };
       };
       exporterTest = ''
-        waitForUnit("prometheus-snmp-exporter.service");
-        waitForOpenPort(9116);
-        succeed("curl -sSf localhost:9116/metrics | grep -q 'snmp_request_errors_total 0'");
+        wait_for_unit("prometheus-snmp-exporter.service")
+        wait_for_open_port(9116)
+        succeed("curl -sSf localhost:9116/metrics | grep -q 'snmp_request_errors_total 0'")
       '';
     };
 
@@ -377,11 +405,11 @@ let
         };
       };
       exporterTest = ''
-        waitForUnit("nginx.service");
-        waitForOpenPort(80);
-        waitForUnit("prometheus-surfboard-exporter.service");
-        waitForOpenPort(9239);
-        succeed("curl -sSf localhost:9239/metrics | grep -q 'surfboard_up 1'");
+        wait_for_unit("nginx.service")
+        wait_for_open_port(80)
+        wait_for_unit("prometheus-surfboard-exporter.service")
+        wait_for_open_port(9239)
+        succeed("curl -sSf localhost:9239/metrics | grep -q 'surfboard_up 1'")
       '';
     };
 
@@ -396,11 +424,11 @@ let
         services.tor.controlPort = 9051;
       };
       exporterTest = ''
-        waitForUnit("tor.service");
-        waitForOpenPort(9051);
-        waitForUnit("prometheus-tor-exporter.service");
-        waitForOpenPort(9130);
-        succeed("curl -sSf localhost:9130/metrics | grep -q 'tor_version{.\\+} 1'");
+        wait_for_unit("tor.service")
+        wait_for_open_port(9051)
+        wait_for_unit("prometheus-tor-exporter.service")
+        wait_for_open_port(9130)
+        succeed("curl -sSf localhost:9130/metrics | grep -q 'tor_version{.\\+} 1'")
       '';
     };
 
@@ -426,10 +454,12 @@ let
         };
       };
       exporterTest = ''
-        waitForUnit("prometheus-varnish-exporter.service");
-        waitForOpenPort(6081);
-        waitForOpenPort(9131);
-        succeed("curl -sSf http://localhost:9131/metrics | grep -q 'varnish_up 1'");
+        wait_for_unit("prometheus-varnish-exporter.service")
+        wait_for_open_port(6081)
+        wait_for_open_port(9131)
+        succeed(
+            "curl -sSf http://localhost:9131/metrics | grep -q 'varnish_up 1'"
+        )
       '';
     };
 
@@ -451,9 +481,11 @@ let
         systemd.services.prometheus-wireguard-exporter.after = [ "wireguard-wg0.service" ];
       };
       exporterTest = ''
-        waitForUnit("prometheus-wireguard-exporter.service");
-        waitForOpenPort(9586);
-        waitUntilSucceeds("curl -sSf http://localhost:9586/metrics | grep '${snakeoil.peer1.publicKey}'");
+        wait_for_unit("prometheus-wireguard-exporter.service")
+        wait_for_open_port(9586)
+        wait_until_succeeds(
+            "curl -sSf http://localhost:9586/metrics | grep '${snakeoil.peer1.publicKey}'"
+        )
       '';
     };
   };
@@ -466,11 +498,13 @@ mapAttrs (exporter: testConfig: (makeTest {
   } testConfig.metricProvider or {}];
 
   testScript = ''
-    ${"$"+exporter}->start();
-    ${concatStringsSep "  " (map (line: ''
-      ${"$"+exporter}->${line};
-    '') (splitString "\n" (removeSuffix "\n" testConfig.exporterTest)))}
-    ${"$"+exporter}->shutdown();
+    ${exporter}.start()
+    ${concatStringsSep "\n" (map (line:
+      if (builtins.substring 0 1 line == " " || builtins.substring 0 1 line == ")")
+      then line
+      else "${exporter}.${line}"
+    ) (splitString "\n" (removeSuffix "\n" testConfig.exporterTest)))}
+    ${exporter}.shutdown()
   '';
 
   meta = with maintainers; {
diff --git a/nixos/tests/radarr.nix b/nixos/tests/radarr.nix
index 9bc5607ccd5..ed90025ac42 100644
--- a/nixos/tests/radarr.nix
+++ b/nixos/tests/radarr.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, ... }:
 
 with lib;
 
@@ -11,8 +11,8 @@ with lib;
     { services.radarr.enable = true; };
 
   testScript = ''
-    $machine->waitForUnit('radarr.service');
-    $machine->waitForOpenPort('7878');
-    $machine->succeed("curl --fail http://localhost:7878/");
+    machine.wait_for_unit("radarr.service")
+    machine.wait_for_open_port("7878")
+    machine.succeed("curl --fail http://localhost:7878/")
   '';
 })
diff --git a/nixos/tests/redis.nix b/nixos/tests/redis.nix
index 325d93424dd..529965d7acd 100644
--- a/nixos/tests/redis.nix
+++ b/nixos/tests/redis.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "redis";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ flokli ];
@@ -15,12 +15,10 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
-
-    $machine->waitForUnit("redis");
-    $machine->waitForOpenPort("6379");
-
-    $machine->succeed("redis-cli ping | grep PONG");
-    $machine->succeed("redis-cli -s /run/redis/redis.sock ping | grep PONG");
+    start_all()
+    machine.wait_for_unit("redis")
+    machine.wait_for_open_port("6379")
+    machine.succeed("redis-cli ping | grep PONG")
+    machine.succeed("redis-cli -s /run/redis/redis.sock ping | grep PONG")
   '';
 })
diff --git a/nixos/tests/redmine.nix b/nixos/tests/redmine.nix
index 2d4df288b05..f0f4cbf6a21 100644
--- a/nixos/tests/redmine.nix
+++ b/nixos/tests/redmine.nix
@@ -64,18 +64,13 @@ let
   };
 in
 {
-  v3-mysql = mysqlTest pkgs.redmine // {
-    name = "v3-mysql";
+  mysql = mysqlTest pkgs.redmine // {
+    name = "mysql";
     meta.maintainers = [ maintainers.aanderse ];
   };
 
-  v4-mysql = mysqlTest pkgs.redmine_4 // {
-    name = "v4-mysql";
-    meta.maintainers = [ maintainers.aanderse ];
-  };
-
-  v4-pgsql = pgsqlTest pkgs.redmine_4 // {
-    name = "v4-pgsql";
+  pgsql = pgsqlTest pkgs.redmine // {
+    name = "pgsql";
     meta.maintainers = [ maintainers.aanderse ];
   };
 }
diff --git a/nixos/tests/roundcube.nix b/nixos/tests/roundcube.nix
index ed0ebd7dd19..4f2560ce8c2 100644
--- a/nixos/tests/roundcube.nix
+++ b/nixos/tests/roundcube.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "roundcube";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ globin ];
@@ -21,10 +21,10 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    $roundcube->start;
-    $roundcube->waitForUnit("postgresql.service");
-    $roundcube->waitForUnit("phpfpm-roundcube.service");
-    $roundcube->waitForUnit("nginx.service");
-    $roundcube->succeed("curl -sSfL http://roundcube/ | grep 'Keep me logged in'");
+    roundcube.start
+    roundcube.wait_for_unit("postgresql.service")
+    roundcube.wait_for_unit("phpfpm-roundcube.service")
+    roundcube.wait_for_unit("nginx.service")
+    roundcube.succeed("curl -sSfL http://roundcube/ | grep 'Keep me logged in'")
   '';
 })
diff --git a/nixos/tests/rss2email.nix b/nixos/tests/rss2email.nix
index 492d47da9f5..d62207a417b 100644
--- a/nixos/tests/rss2email.nix
+++ b/nixos/tests/rss2email.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix {
+import ./make-test-python.nix {
   name = "opensmtpd";
 
   nodes = {
@@ -53,14 +53,14 @@ import ./make-test.nix {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $server->waitForUnit("network-online.target");
-    $server->waitForUnit("opensmtpd");
-    $server->waitForUnit("dovecot2");
-    $server->waitForUnit("nginx");
-    $server->waitForUnit("rss2email");
+    server.wait_for_unit("network-online.target")
+    server.wait_for_unit("opensmtpd")
+    server.wait_for_unit("dovecot2")
+    server.wait_for_unit("nginx")
+    server.wait_for_unit("rss2email")
 
-    $server->waitUntilSucceeds('check-mail-landed >&2');
+    server.wait_until_succeeds("check-mail-landed >&2")
   '';
 }
diff --git a/nixos/tests/shiori.nix b/nixos/tests/shiori.nix
index 0022a7220fe..a5771262c6f 100644
--- a/nixos/tests/shiori.nix
+++ b/nixos/tests/shiori.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ...}:
+import ./make-test-python.nix ({ pkgs, lib, ...}:
 
 {
   name = "shiori";
@@ -8,10 +8,74 @@ import ./make-test.nix ({ lib, ...}:
     { ... }:
     { services.shiori.enable = true; };
 
-  testScript = ''
-    $machine->waitForUnit('shiori.service');
-    $machine->waitForOpenPort('8080');
-    $machine->succeed("curl --fail http://localhost:8080/");
-    $machine->succeed("curl --fail --location http://localhost:8080/ | grep -qi shiori");
+  testScript = let
+    authJSON = pkgs.writeText "auth.json" (builtins.toJSON {
+      username = "shiori";
+      password = "gopher";
+      remember = 1; # hour
+      owner = true;
+    });
+
+  insertBookmark = {
+    url = "http://example.org";
+    title = "Example Bookmark";
+  };
+
+  insertBookmarkJSON = pkgs.writeText "insertBookmark.json" (builtins.toJSON insertBookmark);
+  in ''
+    import json
+
+    machine.wait_for_unit("shiori.service")
+    machine.wait_for_open_port(8080)
+    machine.succeed("curl --fail http://localhost:8080/")
+    machine.succeed("curl --fail --location http://localhost:8080/ | grep -qi shiori")
+
+    with subtest("login"):
+        auth_json = machine.succeed(
+            "curl --fail --location http://localhost:8080/api/login "
+            "-X POST -H 'Content-Type:application/json' -d @${authJSON}"
+        )
+        auth_ret = json.loads(auth_json)
+        session_id = auth_ret["session"]
+
+    with subtest("bookmarks"):
+        with subtest("first use no bookmarks"):
+            bookmarks_json = machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-H 'X-Session-Id:{}'"
+                ).format(session_id)
+            )
+
+            if json.loads(bookmarks_json)["bookmarks"] != []:
+                raise Exception("Shiori have a bookmark on first use")
+
+        with subtest("insert bookmark"):
+            machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-X POST -H 'X-Session-Id:{}' "
+                    "-H 'Content-Type:application/json' -d @${insertBookmarkJSON}"
+                ).format(session_id)
+            )
+
+        with subtest("get inserted bookmark"):
+            bookmarks_json = machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-H 'X-Session-Id:{}'"
+                ).format(session_id)
+            )
+
+            bookmarks = json.loads(bookmarks_json)["bookmarks"]
+            if len(bookmarks) != 1:
+                raise Exception("Shiori didn't save the bookmark")
+
+            bookmark = bookmarks[0]
+            if (
+                bookmark["url"] != "${insertBookmark.url}"
+                or bookmark["title"] != "${insertBookmark.title}"
+            ):
+                raise Exception("Inserted bookmark doesn't have same URL or title")
   '';
 })
diff --git a/nixos/tests/signal-desktop.nix b/nixos/tests/signal-desktop.nix
index 605b9c3e130..c746d46dc55 100644
--- a/nixos/tests/signal-desktop.nix
+++ b/nixos/tests/signal-desktop.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} :
+import ./make-test-python.nix ({ pkgs, ...} :
 
 {
   name = "signal-desktop";
@@ -24,14 +24,14 @@ import ./make-test.nix ({ pkgs, ...} :
   testScript = { nodes, ... }: let
     user = nodes.machine.config.users.users.alice;
   in ''
-    startAll;
-    $machine->waitForX;
+    start_all()
+    machine.wait_for_x()
 
     # start signal desktop
-    $machine->execute("su - alice -c signal-desktop &");
+    machine.execute("su - alice -c signal-desktop &")
 
     # wait for the "Link your phone to Signal Desktop" message
-    $machine->waitForText(qr/Link your phone to Signal Desktop/);
-    $machine->screenshot("signal_desktop");
+    machine.wait_for_text("Link your phone to Signal Desktop")
+    machine.screenshot("signal_desktop")
   '';
 })
diff --git a/nixos/tests/smokeping.nix b/nixos/tests/smokeping.nix
index 07d22805112..4f8f0fcc9fe 100644
--- a/nixos/tests/smokeping.nix
+++ b/nixos/tests/smokeping.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "smokeping";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ cransom ];
@@ -22,12 +22,12 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
-    $sm->waitForUnit("smokeping");
-    $sm->waitForUnit("thttpd");
-    $sm->waitForFile("/var/lib/smokeping/data/Local/LocalMachine.rrd");
-    $sm->succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local");
-    $sm->succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png");
-    $sm->succeed("ls /var/lib/smokeping/cache/index.html");
+    start_all()
+    sm.wait_for_unit("smokeping")
+    sm.wait_for_unit("thttpd")
+    sm.wait_for_file("/var/lib/smokeping/data/Local/LocalMachine.rrd")
+    sm.succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local")
+    sm.succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png")
+    sm.succeed("ls /var/lib/smokeping/cache/index.html")
   '';
 })
diff --git a/nixos/tests/snapper.nix b/nixos/tests/snapper.nix
index 74ec22fd349..018102d7f64 100644
--- a/nixos/tests/snapper.nix
+++ b/nixos/tests/snapper.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ ... }:
+import ./make-test-python.nix ({ ... }:
 {
   name = "snapper";
 
@@ -20,24 +20,16 @@ import ./make-test.nix ({ ... }:
   };
 
   testScript = ''
-    $machine->succeed("btrfs subvolume create /home/.snapshots");
-
-    $machine->succeed("snapper -c home list");
-
-    $machine->succeed("snapper -c home create --description empty");
-
-    $machine->succeed("echo test > /home/file");
-    $machine->succeed("snapper -c home create --description file");
-
-    $machine->succeed("snapper -c home status 1..2");
-
-    $machine->succeed("snapper -c home undochange 1..2");
-    $machine->fail("ls /home/file");
-
-    $machine->succeed("snapper -c home delete 2");
-
-    $machine->succeed("systemctl --wait start snapper-timeline.service");
-
-    $machine->succeed("systemctl --wait start snapper-cleanup.service");
+    machine.succeed("btrfs subvolume create /home/.snapshots")
+    machine.succeed("snapper -c home list")
+    machine.succeed("snapper -c home create --description empty")
+    machine.succeed("echo test > /home/file")
+    machine.succeed("snapper -c home create --description file")
+    machine.succeed("snapper -c home status 1..2")
+    machine.succeed("snapper -c home undochange 1..2")
+    machine.fail("ls /home/file")
+    machine.succeed("snapper -c home delete 2")
+    machine.succeed("systemctl --wait start snapper-timeline.service")
+    machine.succeed("systemctl --wait start snapper-cleanup.service")
   '';
 })
diff --git a/nixos/tests/strongswan-swanctl.nix b/nixos/tests/strongswan-swanctl.nix
index 9bab9349ea7..152c0d61c54 100644
--- a/nixos/tests/strongswan-swanctl.nix
+++ b/nixos/tests/strongswan-swanctl.nix
@@ -16,7 +16,7 @@
 # See the NixOS manual for how to run this test:
 # https://nixos.org/nixos/manual/index.html#sec-running-nixos-tests-interactively
 
-import ./make-test.nix ({ pkgs, ...} :
+import ./make-test-python.nix ({ pkgs, ...} :
 
 let
   allowESP = "iptables --insert INPUT --protocol ESP --jump ACCEPT";
@@ -142,7 +142,7 @@ in {
 
   };
   testScript = ''
-    startAll();
-    $carol->waitUntilSucceeds("ping -c 1 alice");
+    start_all()
+    carol.wait_until_succeeds("ping -c 1 alice")
   '';
 })
diff --git a/nixos/tests/telegraf.nix b/nixos/tests/telegraf.nix
index 6776f8d8c37..73f741b1135 100644
--- a/nixos/tests/telegraf.nix
+++ b/nixos/tests/telegraf.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "telegraf";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ mic92 ];
@@ -22,9 +22,9 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $machine->waitForUnit("telegraf.service");
-    $machine->waitUntilSucceeds("grep -q example /tmp/metrics.out");
+    machine.wait_for_unit("telegraf.service")
+    machine.wait_until_succeeds("grep -q example /tmp/metrics.out")
   '';
 })
diff --git a/nixos/tests/trickster.nix b/nixos/tests/trickster.nix
index 1461a32bb07..e2ca00980d5 100644
--- a/nixos/tests/trickster.nix
+++ b/nixos/tests/trickster.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "trickster";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ "1000101" ];
@@ -15,15 +15,23 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    startAll;
-    $prometheus->waitForUnit("prometheus.service");
-    $prometheus->waitForOpenPort(9090);
-    $prometheus->waitUntilSucceeds("curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'");
-    $trickster->waitForUnit("trickster.service");
-    $trickster->waitForOpenPort(8082);
-    $trickster->waitForOpenPort(9090);
-    $trickster->waitUntilSucceeds("curl -L http://localhost:8082/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'");
-    $trickster->waitUntilSucceeds("curl -L http://prometheus:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'");
-    $trickster->waitUntilSucceeds("curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'");
+    start_all()
+    prometheus.wait_for_unit("prometheus.service")
+    prometheus.wait_for_open_port(9090)
+    prometheus.wait_until_succeeds(
+        "curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+    trickster.wait_for_unit("trickster.service")
+    trickster.wait_for_open_port(8082)
+    trickster.wait_for_open_port(9090)
+    trickster.wait_until_succeeds(
+        "curl -L http://localhost:8082/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+    trickster.wait_until_succeeds(
+        "curl -L http://prometheus:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
+    trickster.wait_until_succeeds(
+        "curl -L http://localhost:9090/metrics | grep 'promhttp_metric_handler_requests_total{code=\"500\"} 0'"
+    )
   '';
 })
\ No newline at end of file
diff --git a/nixos/tests/udisks2.nix b/nixos/tests/udisks2.nix
index dcf869908d8..0cbfa0c4c7b 100644
--- a/nixos/tests/udisks2.nix
+++ b/nixos/tests/udisks2.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
 
 let
 
@@ -30,32 +30,40 @@ in
 
   testScript =
     ''
-      my $stick = $machine->stateDir . "/usbstick.img";
-      system("xz -d < ${stick} > $stick") == 0 or die;
+      import lzma
 
-      $machine->succeed("udisksctl info -b /dev/vda >&2");
-      $machine->fail("udisksctl info -b /dev/sda1");
+      with lzma.open(
+          "${stick}"
+      ) as data, open(machine.state_dir + "/usbstick.img", "wb") as stick:
+          stick.write(data.read())
+
+      machine.succeed("udisksctl info -b /dev/vda >&2")
+      machine.fail("udisksctl info -b /dev/sda1")
 
       # Attach a USB stick and wait for it to show up.
-      $machine->sendMonitorCommand("drive_add 0 id=stick,if=none,file=$stick,format=raw");
-      $machine->sendMonitorCommand("device_add usb-storage,id=stick,drive=stick");
-      $machine->waitUntilSucceeds("udisksctl info -b /dev/sda1");
-      $machine->succeed("udisksctl info -b /dev/sda1 | grep 'IdLabel:.*USBSTICK'");
+      machine.send_monitor_command(
+          f"drive_add 0 id=stick,if=none,file={stick.name},format=raw"
+      )
+      machine.send_monitor_command("device_add usb-storage,id=stick,drive=stick")
+      machine.wait_until_succeeds("udisksctl info -b /dev/sda1")
+      machine.succeed("udisksctl info -b /dev/sda1 | grep 'IdLabel:.*USBSTICK'")
 
       # Mount the stick as a non-root user and do some stuff with it.
-      $machine->succeed("su - alice -c 'udisksctl info -b /dev/sda1'");
-      $machine->succeed("su - alice -c 'udisksctl mount -b /dev/sda1'");
-      $machine->succeed("su - alice -c 'cat /run/media/alice/USBSTICK/test.txt'") =~ /Hello World/ or die;
-      $machine->succeed("su - alice -c 'echo foo > /run/media/alice/USBSTICK/bar.txt'");
+      machine.succeed("su - alice -c 'udisksctl info -b /dev/sda1'")
+      machine.succeed("su - alice -c 'udisksctl mount -b /dev/sda1'")
+      machine.succeed(
+          "su - alice -c 'cat /run/media/alice/USBSTICK/test.txt' | grep -q 'Hello World'"
+      )
+      machine.succeed("su - alice -c 'echo foo > /run/media/alice/USBSTICK/bar.txt'")
 
       # Unmounting the stick should make the mountpoint disappear.
-      $machine->succeed("su - alice -c 'udisksctl unmount -b /dev/sda1'");
-      $machine->fail("[ -d /run/media/alice/USBSTICK ]");
+      machine.succeed("su - alice -c 'udisksctl unmount -b /dev/sda1'")
+      machine.fail("[ -d /run/media/alice/USBSTICK ]")
 
       # Remove the USB stick.
-      $machine->sendMonitorCommand("device_del stick");
-      $machine->waitUntilFails("udisksctl info -b /dev/sda1");
-      $machine->fail("[ -e /dev/sda ]");
+      machine.send_monitor_command("device_del stick")
+      machine.wait_until_fails("udisksctl info -b /dev/sda1")
+      machine.fail("[ -e /dev/sda ]")
     '';
 
 })
diff --git a/nixos/tests/upnp.nix b/nixos/tests/upnp.nix
index 98344aee3ef..d2e7fdd4fbe 100644
--- a/nixos/tests/upnp.nix
+++ b/nixos/tests/upnp.nix
@@ -5,7 +5,7 @@
 # this succeeds an external client will try to connect to the port
 # mapping.
 
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
 
 let
   internalRouterAddress = "192.168.3.1";
@@ -75,20 +75,20 @@ in
   testScript =
     { nodes, ... }:
     ''
-      startAll;
+      start_all()
 
       # Wait for network and miniupnpd.
-      $router->waitForUnit("network-online.target");
-      # $router->waitForUnit("nat");
-      $router->waitForUnit("firewall.service");
-      $router->waitForUnit("miniupnpd");
+      router.wait_for_unit("network-online.target")
+      # $router.wait_for_unit("nat")
+      router.wait_for_unit("firewall.service")
+      router.wait_for_unit("miniupnpd")
 
-      $client1->waitForUnit("network-online.target");
+      client1.wait_for_unit("network-online.target")
 
-      $client1->succeed("upnpc -a ${internalClient1Address} 9000 9000 TCP");
+      client1.succeed("upnpc -a ${internalClient1Address} 9000 9000 TCP")
 
-      $client1->waitForUnit("httpd");
-      $client2->waitUntilSucceeds("curl http://${externalRouterAddress}:9000/");
+      client1.wait_for_unit("httpd")
+      client2.wait_until_succeeds("curl http://${externalRouterAddress}:9000/")
     '';
 
 })
diff --git a/nixos/tests/xautolock.nix b/nixos/tests/xautolock.nix
index ee46d9e05b0..10e92b40e95 100644
--- a/nixos/tests/xautolock.nix
+++ b/nixos/tests/xautolock.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 
 with lib;
 
@@ -15,10 +15,10 @@ with lib;
   };
 
   testScript = ''
-    $machine->start;
-    $machine->waitForX;
-    $machine->mustFail("pgrep xlock");
-    $machine->sleep(120);
-    $machine->mustSucceed("pgrep xlock");
+    machine.start()
+    machine.wait_for_x()
+    machine.fail("pgrep xlock")
+    machine.sleep(120)
+    machine.succeed("pgrep xlock")
   '';
 })
diff --git a/nixos/tests/xdg-desktop-portal.nix b/nixos/tests/xdg-desktop-portal.nix
deleted file mode 100644
index 79ebb83c49a..00000000000
--- a/nixos/tests/xdg-desktop-portal.nix
+++ /dev/null
@@ -1,17 +0,0 @@
-# run installed tests
-import ./make-test.nix ({ pkgs, ... }:
-
-{
-  name = "xdg-desktop-portal";
-  meta = {
-    maintainers = pkgs.xdg-desktop-portal.meta.maintainers;
-  };
-
-  machine = { pkgs, ... }: {
-    environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-  };
-
-  testScript = ''
-    $machine->succeed("gnome-desktop-testing-runner -d '${pkgs.xdg-desktop-portal.installedTests}/share'");
-  '';
-})
diff --git a/nixos/tests/yabar.nix b/nixos/tests/yabar.nix
index bbc0cf4c7dd..9108004d4df 100644
--- a/nixos/tests/yabar.nix
+++ b/nixos/tests/yabar.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 
 with lib;
 
@@ -20,14 +20,14 @@ with lib;
   };
 
   testScript = ''
-    $machine->start;
-    $machine->waitForX;
+    machine.start()
+    machine.wait_for_x()
 
     # confirm proper startup
-    $machine->waitForUnit("yabar.service", "bob");
-    $machine->sleep(10);
-    $machine->waitForUnit("yabar.service", "bob");
+    machine.wait_for_unit("yabar.service", "bob")
+    machine.sleep(10)
+    machine.wait_for_unit("yabar.service", "bob")
 
-    $machine->screenshot("top_bar");
+    machine.screenshot("top_bar")
   '';
 })
diff --git a/nixos/tests/zookeeper.nix b/nixos/tests/zookeeper.nix
index f343ebd39e4..42cf20b39c5 100644
--- a/nixos/tests/zookeeper.nix
+++ b/nixos/tests/zookeeper.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "zookeeper";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus ];
@@ -15,14 +15,20 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $server->waitForUnit("zookeeper");
-    $server->waitForUnit("network.target");
-    $server->waitForOpenPort(2181);
+    server.wait_for_unit("zookeeper")
+    server.wait_for_unit("network.target")
+    server.wait_for_open_port(2181)
 
-    $server->waitUntilSucceeds("${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 create /foo bar");
-    $server->waitUntilSucceeds("${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 set /foo hello");
-    $server->waitUntilSucceeds("${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 get /foo | grep hello");
+    server.wait_until_succeeds(
+        "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 create /foo bar"
+    )
+    server.wait_until_succeeds(
+        "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 set /foo hello"
+    )
+    server.wait_until_succeeds(
+        "${pkgs.zookeeper}/bin/zkCli.sh -server localhost:2181 get /foo | grep hello"
+    )
   '';
 })