diff --git a/common/README.md b/common/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..db4ccc76ea4d7b33c463e8007a76bbc05931e328
--- /dev/null
+++ b/common/README.md
@@ -0,0 +1,29 @@
+Common files shared accross apertis tests.
+
+Those files are shared through a git subtree which is included by all test projects.
+
+Use `common-subtree.sh add` to add the subtree in a tests repository.
+For example:
+```
+# Retrieve the common-subtree.sh script, we use `git`, but could also use `wget`
+$ git clone git@gitlab.apertis.org:tests/common
+
+# Clone the test repository if needed
+$ git clone git@gitlab.apertis.org:tests/iptables-basic
+# Enter the test directory
+$ cd iptables-basic
+
+# In the test directory, add the common git subtree
+$ ../common/common-subtree.sh add
+
+# A new commit has been created, push the test with the subtree initialized 
+$ git push origin
+```
+
+If a script is supposed to be in the common/ directory, it should be commited in the
+[common repository](https://gitlab.apertis.org/tests/common), and then it can be pulled
+into the git subtree using the command:
+```
+$ ../common/common-subtree.sh pull
+
+```
diff --git a/common/common-apparmor.sh b/common/common-apparmor.sh
new file mode 100644
index 0000000000000000000000000000000000000000..f6a94977038e82708ec2f870dde7df9f396828f6
--- /dev/null
+++ b/common/common-apparmor.sh
@@ -0,0 +1,43 @@
+# Need to source this file.
+
+# Pass in audit string, returns formatted block as `aa_log_extract_tokens.pl`
+# would have provided. Need to return:
+#
+# profile:/usr/bin/busctl
+# sdmode:REJECTING
+# denied_mask:r
+# operation:open
+# name:/proc/879/stat
+# request_mask:r
+#
+# Note: apparmor uses the term "DENIED" rather than "REJECTING", but our
+# expected values want "REJECTING", so tweak for now.
+apparmor_parse_journal() {
+	awk -v event=$2 '
+		{
+			for (i = 1; i <= NF; i++) {
+				n = index($i, "=");
+				if(n) {
+					vars[substr($i, 1, n - 1)] = substr($i, n + 1)
+					# Strip quotes
+					gsub("\"","",vars[substr($i, 1, n - 1)])
+				}
+			}
+		}
+		{
+			if (vars["apparmor"] == event) {
+				print "===="
+				print "profile:"vars["profile"]
+				# Match old nomenclature
+				if (vars["apparmor"] == "DENIED") {
+					vars["apparmor"] = "REJECTING"
+				}
+				print "sdmode:"vars["apparmor"]
+				print "denied_mask:"vars["denied_mask"]
+				print "operation:"vars["operation"]
+				print "name:"vars["name"]
+				print "request_mask:"vars["requested_mask"]
+			}
+		}
+	' $1
+}
diff --git a/common/common-subtree.sh b/common/common-subtree.sh
new file mode 100755
index 0000000000000000000000000000000000000000..59c5e21b3d73466f45e92bef5c038a13600c297f
--- /dev/null
+++ b/common/common-subtree.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+usage () {
+	echo "usage: $0 [add|pull]"
+	echo ""
+	echo "'$0 add' adds the common subtree to a tests repository"
+	echo "'$0 pull' is used to update the common subtree in tests"
+}
+
+case $1 in
+pull)
+git subtree pull -P common git@gitlab.apertis.org:tests/common.git master
+;;
+add)
+git subtree add -P common git@gitlab.apertis.org:tests/common.git master
+;;
+*) usage;;
+esac
diff --git a/common/common.sh b/common/common.sh
new file mode 100644
index 0000000000000000000000000000000000000000..913e1bfc3a57549fe8e68453dade06e4fc3ad60c
--- /dev/null
+++ b/common/common.sh
@@ -0,0 +1,319 @@
+# Source me!
+# I need ROOTDIR, PN and PV to be defined! (see bottom of file)
+# vim: set sts=4 sw=4 et :
+
+# Detect whether stdout is to a terminal
+if tty -s <&1; then
+    IS_TTY="true"
+fi
+
+is_tty() {
+    [ "${IS_TTY}" = true ] && return 0
+    return 1
+}
+
+##########
+# Murder #
+##########
+_kill_daemons() {
+    pkill dconf-service >/dev/null 2>&1 || true
+    return 0 # Never fail
+}
+
+##########
+# Output #
+##########
+cry_n() {
+    if is_tty; then
+        # Cry tears of blood
+        /bin/echo -n -e "\033[01;31m#\033[00m $@"
+    else
+        /bin/echo -n -e "# $@"
+    fi
+}
+
+cry() {
+    cry_n "$@"
+    echo
+}
+
+say_n() {
+    if is_tty; then
+        # Speak in green
+        /bin/echo -n -e "\033[01;32m#\033[00m $@"
+    else
+        /bin/echo -n -e "# $@"
+    fi
+}
+
+say() {
+    say_n "$@"
+    echo
+}
+
+whine_n() {
+    if is_tty; then
+        # Whine in yellow
+        /bin/echo -n -e "\033[01;33m#\033[00m $@"
+    else
+        /bin/echo -n -e "# $@"
+    fi
+}
+
+whine() {
+    whine_n "$@"
+    echo
+}
+
+echo_red() {
+    if is_tty; then
+        # Print text in red, without an implicit newline
+        /bin/echo -n -e "\033[01;31m$@\033[00m"
+    else
+        /bin/echo -n -e "$@"
+    fi
+}
+
+echo_green() {
+    if is_tty; then
+        # Print text in green, without an implicit newline
+        /bin/echo -n -e "\033[01;32m$@\033[00m"
+    else
+        /bin/echo -n -e "$@"
+    fi
+}
+
+###################
+# Status Messages #
+###################
+test_success() {
+    say "All tests PASSED successfully!"
+    _kill_daemons
+    trap - ERR 2>/dev/null || true
+    trap - EXIT
+    exit 0
+}
+
+test_failure() {
+    [ $? -eq 0 ] && exit
+    cry "Tests FAILED!"
+    _kill_daemons
+    whine "Work directory was: ${WORKDIR}"
+    exit 0
+}
+
+setup_success() {
+    say "Test setup successfully!"
+    _kill_daemons
+    trap - ERR 2>/dev/null || true
+    trap - EXIT
+    return 0
+}
+
+setup_failure() {
+    [ $? -eq 0 ] && exit
+    cry "Test setup failed!"
+    _kill_daemons
+    exit 1
+}
+
+#############
+# Utilities #
+#############
+create_temp_workdir() {
+    local tempdir="$(mktemp -d -p ${WORKDIR})"
+    echo "${tempdir}" 1>&2
+    _realpath "${tempdir}"
+}
+
+# Takes an IFS delimited list of files from stdin, checks if each exists, and
+# passes it on if it does. If the file doesn't exist, it prefixes the filename
+# with "DNE", which is caught by _src_test, which then marks it as a failed test
+check_file_exists_tee() {
+    while read i; do
+        if [ -e "$i" ]; then
+            echo "$i"
+        else
+            echo "DNE: $i"
+        fi
+    done
+}
+
+check_not_root() {
+    if [ "$(id -ru)" = 0 ]; then
+        cry "Do not run this test as root!"
+        return 1
+    fi
+    return 0
+}
+
+check_have_root() {
+    if [ "$(id -ru)" != 0 ]; then
+        cry "Need root to run this test successfully!"
+        return 1
+    fi
+    return 0
+}
+
+arch_is_arm() {
+    case "$(uname -m)" in
+        arm*)
+            return 0
+            ;;
+    esac
+    return 1
+}
+
+# Check if a session bus is running otherwise fail
+ensure_dbus_session() {
+    local dbus_socket
+
+    dbus_socket="/run/user/$(id -ru)/bus"
+    if [ ! -e "${dbus_socket}" ]; then
+        cry "Could not find session bus..."
+        return 1
+    fi
+
+    return 0
+}
+
+######################
+# Internal functions #
+######################
+# Sleep, unless specified otherwise
+_sleep() {
+    if [ -z "${QUICK}" ]; then
+        /bin/sleep "$@"
+    else
+        /bin/sleep 0.2
+    fi
+}
+
+# Run external utilities with this
+# All output is suppressed by default as LAVA doesn't cope well with very large logs.
+# Use DEBUG=1 to enable all output when running tests locally on developer devices.
+# Short-circuiting with a return is needed to prevent unexpected exiting due to
+# a non-zero return code when set -e is enabled.
+_run_cmd() {
+    if [ "${DEBUG}" = 1 ]
+    then
+        "$@" || return
+    else
+        "$@" >/dev/null 2>&1 || return
+    fi
+}
+
+_expect_pass() {
+    _run_cmd "$@"
+}
+
+_expect_fail() {
+    ! _run_cmd "$@"
+}
+
+_src_test() {
+    # Reads a list of tests to run via stdin, and executes them one by one
+    # All these are supposed to pass
+    local i
+    local failed=
+    local expect=$1
+    local prefix=""
+    shift
+
+    if [ -n "${APERTIS_TESTS_NAME_PREFIX}" ]; then
+        prefix="${APERTIS_TESTS_NAME_PREFIX}"
+    fi
+
+    while read i; do
+        case $i in
+          [#]*)
+            echo_red "${prefix}$i: skip\n"
+            continue
+            ;;
+          # See check_file_exists_tee()
+          DNE*)
+            echo_red "${prefix}$i: fail\n"
+            whine "Got an invalid executable name '$i'!"
+            failed="$failed $i"
+            continue
+            ;;
+        esac
+        say "Running test '$i' ..."
+        if ! "$expect" "$i" "$@"; then
+            failed="$failed $i"
+            echo_red "${prefix}$i: fail\n"
+        else
+            echo_green "${prefix}$i: pass\n"
+        fi
+    done
+    if [ -n "$failed" ]; then
+        whine "The following tests failed: "
+        for i in $failed; do
+            whine "\t$i"
+        done
+        return 1
+    fi
+}
+
+# Let's not depend on realpath; we don't need it
+_realpath() {
+    cd "$1"
+    echo "$PWD"
+}
+
+##########
+# Phases #
+##########
+src_test_pass() {
+    _src_test _expect_pass "$@"
+}
+
+src_test_fail() {
+    _src_test _expect_fail "$@"
+}
+
+src_unpack() {
+    mkdir -p "${WORKDIR}"
+}
+
+src_cleanup() {
+    rm -rf "${WORKDIR}"
+}
+
+src_copy() {
+    # Copy $1 $2 .. $N-1 to $N
+    # Essentially just a wrapper around `cp` right now
+    cp -v -L -r "$@" 1>&2
+}
+
+src_copy_contents() {
+    # Copy the contents of $1 to $2
+    # FIXME: This ignores dot-files. Use rsync or something?
+    cp -v -L -r "$1"/* "$2" 1>&2
+}
+
+#############
+# Variables #
+#############
+# Fix this to not flood /var/tmp with temporary directories
+BASEWORKDIR="/var/tmp/chaiwala-tests"
+mkdir -p "${BASEWORKDIR}"
+WORKDIR="$(mktemp -d -p ${BASEWORKDIR} "$(date +%Y%m%d-%H%M%S)-XXXXXXXXXX")"
+# Tests might be run as chaiwala, or as root, or some other user
+# Everyone should be able to write here
+chown 1000 "${BASEWORKDIR}"
+chmod 777 "${BASEWORKDIR}" || true
+chmod 777 "${WORKDIR}" || true
+sync
+
+# Wrappers for external commands used
+WGET="${WGET:-wget -c}"
+# We disable apt-get, and just do a pass-through because these tests are
+# integrated into LAVA now
+#APT_GET="$(type -P true)"
+GDBUS="${GDBUS:-gdbus}"
+
+# 0 = no output
+# 1 = stderr
+# 2 = stdout + stderr
+DEBUG="${DEBUG:-0}"
diff --git a/common/inherit-config.sh b/common/inherit-config.sh
new file mode 100644
index 0000000000000000000000000000000000000000..050b5f1054870804866d823c8f805e55ff0ef462
--- /dev/null
+++ b/common/inherit-config.sh
@@ -0,0 +1,21 @@
+# This file should be symlinked inside your test directory,
+# and sourced from config.sh
+# It will be modified and copied to the install directory by make
+
+. "${TESTDIR}/common/common.sh"
+
+## These variables are properties of the image itself
+LIBEXECDIR="${LIBEXECDIR:-/usr/lib}"
+
+## These variables get modified by `make` during install
+# Directory where scripts and other non-binary data is installed
+TESTDATADIR="${TESTDIR}"
+# Binary executable install directory
+TESTLIBDIR="${TESTDIR}"
+
+REALTESTDIR="$(_realpath ${TESTDIR})"
+# Directory where test data/resources are installed
+RESOURCE_DIR="${REALTESTDIR}/common/resources"
+
+## These are derived from the above
+MEDIA_RESOURCE_DIR="${RESOURCE_DIR}/media"
diff --git a/common/run-aa-test b/common/run-aa-test
new file mode 100755
index 0000000000000000000000000000000000000000..347676ee8b7a4d7749d4fdb00ca7159c7925b984
--- /dev/null
+++ b/common/run-aa-test
@@ -0,0 +1,182 @@
+#!/bin/sh
+#
+# Copyright © 2018 Collabora Ltd.
+#
+# Based on python version of run-aa-test
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+PWD=$(cd $(dirname $0); pwd; cd - >/dev/null 2>&1)
+. ${PWD}/common-apparmor.sh
+
+ALTERNATIVE_SEPARATOR="## alternative ##"
+END=2
+
+case $(echo ${LAUNCH_DBUS} | tr [A-Z] [a-z]) in
+0 | no | false)
+	LAUNCH_DBUS="False"
+	;;
+*)
+	LAUNCH_DBUS="True"
+esac
+
+case $(echo ${RUN_AS_USER} | tr [A-Z] [a-z]) in
+0 | no | false)
+	RUN_AS_USER="False"
+	;;
+*)
+	RUN_AS_USER="True"
+esac
+
+CHAIWALA_UID=1000
+CHAIWALA_USER="user"
+
+# Check parameters
+if [ $# -lt 2 ]; then
+
+	echo "Usage: run-aa-test <expectation-file> <command> <argument-1> <argument-2> …"
+	echo "\"export LAUNCH_DBUS=no\" in the test script to not launch a dbus session."
+	echo "\"export RUN_AS_USER=no\" in the test script to not run as ${CHAIWALA_USER}"
+	exit 1
+fi
+
+EXPECT_FILE=$1
+shift
+
+if [ ! -r ${EXPECT_FILE} ]; then
+	echo "Cannot read specified expectation file: ${EXPECT_FILE}"
+	exit 1
+fi
+
+if [ ! -x $1 ]; then
+	echo "Cannot execute specified test executable: $1"
+	exit 1
+fi
+
+# typically "normal.expected" or "malicious.expected"
+TEST_TITLE=$( basename ${EXPECT_FILE} )
+
+# Touch .bash_history, which we use in some tests, if it's not there.
+bash_history="/home/${CHAIWALA_USER}/.bash_history"
+if [ ! -r ${bash_history} ]; then
+	sudo -u ${CHAIWALA_USER} touch ${bash_history}
+	RET=$?
+	if [ "$RET" != "0" ]; then
+		echo "Failed to create .bash_history: $RET"
+		exit 1
+	fi
+fi
+
+# Create a temporary directory for files
+TMP_DIR=$(mktemp -d)
+
+# Log start time
+START_TIME=$(date +"%F %T")
+
+if [ "${LAUNCH_DBUS}" = "True" ]; then
+	# Start a new D-Bus session for this test
+	CMD="dbus-run-session -- $*"
+else
+	CMD="$*"
+fi
+
+CMDLINE="${PWD}/run-test-in-systemd"
+if [ ! -x $CMDLINE ]; then
+	echo "common/run-test-in-systemd not found"
+	exit 1
+fi
+
+CMDLINE="${CMDLINE}"
+
+if [ "${RUN_AA_TEST_TIMEOUT}" != "" ]; then
+	CMDLINE="${CMDLINE} --timeout=${RUN_AA_TEST_TIMEOUT}"
+fi
+
+if [ "${RUN_AS_USER}" = "True" ]; then
+	CMDLINE="${CMDLINE} --user=${CHAIWALA_UID}"
+else
+	CMDLINE="${CMDLINE} --system"
+fi
+
+CMDLINE="${CMDLINE} ${CMD}"
+
+echo "#=== running test script: ${CMDLINE} ==="
+
+setsid ${CMDLINE}
+RET=$?
+
+echo "#--- end of test script, status: ${RET}"
+
+if [ "${RET}" = "0" ]; then
+	echo "${TEST_TITLE}_underlying_tests: pass"
+else
+	echo "# ${CMDLINE} exited ${RET}"
+	# typically "normal.expected_underlying_tests: fail"
+	echo "${TEST_TITLE}_underlying_tests: fail"
+
+	exit 1
+fi
+
+# Give journal time to log the entries.
+sleep 3
+
+# Get audit information from journal
+AUDIT_FILE=${TMP_DIR}/AUDIT
+journalctl -S "${START_TIME}" -t audit -o cat > ${AUDIT_FILE}
+
+echo "#=== ${TEST_TITLE} ==="
+
+echo "#---8<--- raw apparmor output from journal"
+cat ${AUDIT_FILE} | sed 's/^/# /'
+echo "#--->8---"
+
+echo "#---8<--- expected parsed apparmor output from journal"
+cat ${EXPECT_FILE} | sed 's/^/# /'
+echo "#--->8---"
+
+csplit ${EXPECT_FILE} -f ${TMP_DIR}/EXPECT -b "%d" "/^${ALTERNATIVE_SEPARATOR}$/" {*}
+
+# Old versions of csplit don't provide "--suppress-matched", strip separator separately
+for FILE in ${TMP_DIR}/EXPECT*; do
+	sed -i "/^${ALTERNATIVE_SEPARATOR}$/d" ${FILE}
+done
+
+PARSE_FILE="${TMP_DIR}/PARSE"
+
+# TODO: There is potential for other processes to cause messages to appear in
+#       the journal that may lead to false failures. If this is found to be an
+#       issue in practice, then additional filtering of results may be required
+apparmor_parse_journal ${AUDIT_FILE} DENIED > ${PARSE_FILE}
+
+echo "#---8<--- actual parsed apparmor output from journal"
+cat ${PARSE_FILE} | sed 's/^/# /'
+echo "#--->8---"
+
+MATCH_EXPECTATION="False"
+
+# We might have alternative expectations, take that into consideration.
+OUTPUT_MD5=$( cat ${PARSE_FILE} | md5sum )
+COUNT=$( ls -1 ${TMP_DIR}/EXPECT* | wc -l )
+NUM=0
+while [ $((${NUM} < ${COUNT})) = 1 ]; do
+	EXPECTED_MD5=$( cat ${TMP_DIR}/EXPECT${NUM} | md5sum )
+	if [ "${OUTPUT_MD5}" = "${EXPECTED_MD5}" ]; then
+		echo "# audit log matches alternative expectation ${NUM}/${COUNT}"
+		MATCH_EXPECTATION="True"
+	fi
+	NUM=$((${NUM}+1))
+done
+
+if [ "${MATCH_EXPECTATION}" = "True" ]; then
+	echo "${TEST_TITLE}: pass"
+else
+	echo "#---8<--- diff"
+	diff -urN ${TMP_DIR}/EXPECT${NUM} ${PARSE_FILE}
+	echo "#--->8---"
+	echo "${TEST_TITLE}: fail"
+	exit 1
+fi
+
+exit 0
diff --git a/common/run-test-in-systemd b/common/run-test-in-systemd
new file mode 100755
index 0000000000000000000000000000000000000000..ba2c3addc3be213cdf31b395e06f5e505c5f5d7a
--- /dev/null
+++ b/common/run-test-in-systemd
@@ -0,0 +1,330 @@
+#!/bin/sh
+
+# run-test-in-systemd.sh — run a LAVA test as a systemd unit
+#
+# Copyright © 2015, 2016, 2017 Collabora Ltd.
+#
+# SPDX-License-Identifier: MPL-2.0
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+usage='
+Usage:
+  run-test-in-systemd [OPTIONS...] [--] COMMAND [ARGS...]
+                        Run COMMAND ARGS as a systemd unit
+Options:
+  --system|--user=UID   Run as a system or user service
+                        (default: current user, or --system if run as root)
+  --name=NAME           Use NAME as the test-case name, escaping special
+                        characters (default: COMMAND ARGS)
+  --basename            Use the non-directory part of COMMAND as the test-case
+                        name for LAVA, stripping extensions .pl, .py and/or
+                        .sh and escaping special characters
+  --timeout=EXPR        Set a timeout (systemd syntax is allowed, e.g. 300,
+                        300s, 5min, 300000ms are equivalent; default 15min)
+  --chdir=DIRNAME       Change current working directory to DIRNAME
+  --exit-status         Exit with the status of COMMAND
+  --no-lava             Do not integrate with LAVA (implies --exit-status)
+  --debug               Show journal messages
+  --full-debug          Show full journal logs
+'
+
+set -e
+
+as_target_user=
+debug=
+full_debug=
+journalctl="journalctl --no-pager --full"
+lava_runes=1
+name=""
+progname="$(basename "$0")"
+systemctl="systemctl"
+target_user=
+time_adverb=""
+timeout=15min
+uid="$(id -u)"
+use_basename=
+use_exit_status=
+working_dir="$(pwd)"
+
+debug () {
+    [ -z "${debug}" ] || echo "# ${progname}: $*" >&2
+}
+
+die () {
+    echo "${progname}: ERROR: $*" >&2
+    exit 1
+}
+
+systemd_escape_argument () {
+    # Escape an argument for ExecStart
+    # - escape % as %%, $ as $$ and \ as \\
+    # - escape single and double quotes
+    # - escape control characters, delete and non-ASCII like \xff
+    # I can't work out how to do the latter in sh so I'm resorting to Perl
+    perl -e '$_ = $ARGV[0];
+        s/([\\%\$])/$1$1/g;
+        s/([\x22\x27])/\\$1/g;
+        s/([^\x20-\x7e])/sprintf(q(\\x%02x), ord $1)/ge;
+        print qq("$_");
+    ' -- "$1"
+}
+
+if [ "x${uid}" != x0 ]
+then
+    target_user="${uid}"
+    journalctl="sudo ${journalctl}"
+fi
+
+if which busybox >/dev/null; then
+    time_adverb="busybox time"
+elif which time >/dev/null; then
+    time_adverb="time"
+else
+    time_adverb=""
+fi
+
+if ! which "lava-test-case" >/dev/null; then
+    lava_runes=
+    use_exit_status=1
+fi
+
+# -o + means stop parsing at the first non-option argument, so we can do
+# things like: run-test-in-systemd sh -c 'echo hello'
+args="$(getopt \
+    -o '+h' \
+    -l "basename,chdir:,debug,exit-status,full-debug,help,name:,no-lava,system,timeout:,user:" \
+    -n "${progname}" -- "$@")"
+eval set -- "${args}"
+
+while [ "$#" -gt 0 ]; do
+    case "$1" in
+        (--basename)
+            use_basename=1
+            shift
+            ;;
+        (--debug)
+            debug=1
+            shift
+            ;;
+        (--exit-status)
+            use_exit_status=1
+            shift
+            ;;
+	(--full-debug)
+	    full_debug=1
+	    shift
+	    ;;
+        (--help|-h)
+            echo "${usage}"
+            exit 0
+            ;;
+        (--name)
+            name="$2"
+            shift 2
+            ;;
+        (--no-lava)
+            lava_runes=
+            use_exit_status=1
+            shift
+            ;;
+        (--system)
+            target_user=
+            as_uid=
+            shift
+            ;;
+        (--timeout)
+            timeout="$2"
+            shift 2
+            ;;
+        (--user)
+            # $2 is a numeric uid or a username
+            target_user="$(getent passwd "$2")" || die "user not found: $2"
+            # accept a username or a uid, convert to a uid
+            target_user="$(echo "${target_user}" | cut -d: -f3)"
+            shift 2
+            ;;
+	(--chdir)
+	    working_dir="$2"
+	    shift 2
+	    ;;
+        (--)
+            shift
+            break
+            ;;
+        (*)
+            break
+            ;;
+    esac
+done
+
+[ "$#" -gt 0 ] || die "a command is required"
+
+if [ -n "${use_basename}" ]; then
+    name="$1"
+    name="${name##*/}"
+    name="${name%.pl}"
+    name="${name%.py}"
+    name="${name%.sh}"
+elif [ -z "${name}" ]; then
+    name="$*"
+fi
+
+my_dir="$(dirname "$0")"
+my_dir="$(cd "$my_dir" && pwd)"
+tee_exec="${my_dir}/tee-exec"
+
+name="$(echo -n "${name}" | tr -c -s 'A-Za-z0-9---_' '_' | head -c 220)"
+service="generated-test-case-${name}.service"
+
+if [ -n "${target_user}" ]; then
+    debug "running test as a user service for uid ${target_user}"
+
+    # We've probably been invoked in some stack of sudo and su
+    # commands, so our login environment is probably nonsense;
+    # fix it so we can contact systemd.
+    #
+    # This environment is only used to contact systemd, so it is
+    # safe for it to be rather minimal. The actual test runs in
+    # the environment provided by systemd, as documented in systemd.exec(5)
+    # and potentially augmented by the SetEnvironment() call.
+    #
+    # System services can rely on having PATH and LANG.
+    #
+    # User services can additionally rely on SHELL, USER, LOGNAME, HOME,
+    # MANAGERPID, XDG_RUNTIME_DIR.
+    #
+    # Non-systemd components in Apertis currently set DISPLAY and
+    # DBUS_SESSION_BUS_ADDRESS, although the former may be lost in the
+    # migration to Wayland and the latter is redundant with XDG_RUNTIME_DIR
+    # (our three supported D-Bus implementations - GDBus, libdbus and sd-bus -
+    # all default to unix:path=$XDG_RUNTIME_DIR/bus).
+    XDG_RUNTIME_DIR="/run/user/${target_user}"
+    export XDG_RUNTIME_DIR
+    unset DBUS_SESSION_BUS_ADDRESS
+
+    if ! [ -S "${XDG_RUNTIME_DIR}/bus" ]; then
+        die "${XDG_RUNTIME_DIR}/bus does not exist, cannot proceed"
+    fi
+
+    if [ "${uid}" != "${target_user}" ]; then
+        as_target_user="sudo -u #${target_user} env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}"
+    fi
+
+    filename="${XDG_RUNTIME_DIR}/systemd/user"
+    debug "creating ${filename}"
+    ${as_target_user} install -m 700 -d "${filename}"
+    filename="${filename}/${service}"
+    systemctl="systemctl --user"
+else
+    debug "running test as a system service"
+    filename="/run/systemd/system/${service}"
+    if [ "$(id -ru)" != 0 ]; then
+        as_target_user="sudo"
+    fi
+fi
+
+systemctl="${as_target_user} ${systemctl}"
+
+debug "getting Journal cursor"
+cursor="$(${journalctl} --show-cursor -n 0 -o cat)"
+cursor="${cursor#-- cursor: }"
+debug "Journal cursor: ${cursor}"
+
+${as_target_user} rm -f "${filename}.tmp"
+${as_target_user} rm -f "${filename}"
+
+user_log_dir="$(${as_target_user} mktemp -d)"
+# We write the output to a fifo (named pipe) so that we can watch it in
+# real-time, below. We start watching it before we start the test.
+${as_target_user} mkfifo "${user_log_dir}/stdout.fifo"
+exec_start="${tee_exec} ${user_log_dir}/stdout.fifo"
+
+while [ "$#" -gt 0 ]; do
+    exec_start="${exec_start} $(systemd_escape_argument "$1")"
+    shift
+done
+
+debug "creating ${filename} to run ${exec_start}"
+
+# Not using systemd-run because it doesn't give us control over
+# the timeout, and systemd is better at enforcing those than LAVA is.
+# Use tee to get a sudo'able pseudo-redirection.
+${as_target_user} tee ${filename}.tmp >/dev/null <<EOF
+[Service]
+Type=oneshot
+TimeoutSec=${timeout}
+WorkingDirectory=${working_dir}
+ExecStart=${exec_start}
+StandardOutput=journal
+StandardError=inherit
+SyslogIdentifier=${service}
+Restart=no
+EOF
+${as_target_user} mv "${filename}.tmp" "${filename}"
+
+if [ -n "${debug}" ]; then
+    debug ".service file:"
+    sed -e "s/^/# ${progname}:  /" < ${filename} >&2
+fi
+
+debug "reloading via ${systemctl} daemon-reload"
+${systemctl} daemon-reload
+${systemctl} stop ${service} || :
+
+debug "starting test"
+
+# Watch the output in real time, both for developers' benefit and so that
+# LAVA does not think "it has become inactive" and kill us.
+${as_target_user} cat "${user_log_dir}/stdout.fifo" &
+cat_fifo_pid="$!"
+
+if ${time_adverb} ${systemctl} start ${service}; then
+    debug "test passed"
+    result=pass
+    exit_status="0"
+else
+    exit_status="$?"
+    if [ "${exit_status}" = 77 ]; then
+        debug "test skipped: $?"
+        result=skip
+    else
+        debug "test failed: $?"
+        result=fail
+    fi
+fi
+
+debug "killing any leftover test processes"
+${systemctl} stop ${service} || :
+
+debug "waiting for end-of-file on command output..."
+wait "$cat_fifo_pid" || result=fail
+rm -fr "${user_log_dir}" || result=fail
+
+if [ -n "${lava_runes}" ]; then
+    "lava-test-case" "${name}" --result "${result}"
+fi
+
+# Show journal logs if there is an error and any of the `debug` flag was passed,
+# otherwise suggest to use debug flags to show more information.
+if [ "${exit_status}" != 0 ]; then
+    if [ -n "${full_debug}" ]; then
+	debug "Showing full logs (--full-debug passed)"
+	${journalctl} -o verbose -b -0 --after-cursor "${cursor}" || :
+    elif [ -n "${debug}" ]; then
+	debug "Showing logs (--debug passed)"
+	${journalctl} -b -0 --after-cursor "${cursor}" || :
+    else
+	echo "NOTE: Run command with the --debug or --full-debug option to enable journal logs"
+    fi
+fi
+
+${as_target_user} rm -f "${filename}.tmp"
+${as_target_user} rm -f "${filename}"
+
+if [ -n "${use_exit_status}" ]; then
+    exit "${exit_status}"
+fi
+
+# vim:set sw=4 sts=4 et:
diff --git a/common/tee-exec b/common/tee-exec
new file mode 100755
index 0000000000000000000000000000000000000000..f793ee5bd8f17ebc00dd2f4783c8cd705b51ebe7
--- /dev/null
+++ b/common/tee-exec
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# tee-exec — like tee, but execute arguments.
+# Used by run-test-in-systemd.
+#
+# Copyright © 2015 Collabora Ltd.
+#
+# SPDX-License-Identifier: MPL-2.0
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+log="$1"
+shift
+
+# POSIX shell compatible pipefail replacement for the command below:
+#   set -o pipefail && "$@" 2>&1 | tee -- "$log"
+# See https://www.spinics.net/lists/dash/msg00165.html
+exec 3>&1; s=$(exec 4>&1 >&3; { "$@" 2>&1 ; echo $? >&4; } | tee -- "$log" ) && exit $s
diff --git a/common/update-test-path b/common/update-test-path
new file mode 100644
index 0000000000000000000000000000000000000000..61174ffba24e5f9d5fa56269cc22b3911bde093e
--- /dev/null
+++ b/common/update-test-path
@@ -0,0 +1,16 @@
+# Current test directory extracted from pwd
+TESTPATH=$(pwd)
+
+# Path for architecture independant binaries
+PATH=${TESTPATH}/common:${TESTPATH}/bin:$PATH
+
+# Path for architecture specific binaries
+case `uname -m` in
+x86_64)  ARCHDIR=amd64 ;;
+armv7l)  ARCHDIR=armhf ;;
+aarch64) ARCHDIR=arm64 ;;
+esac
+
+PATH=${TESTPATH}/${ARCHDIR}/bin:$PATH
+export LD_LIBRARY_PATH=${TESTPATH}/${ARCHDIR}/lib:$LD_LIBRARY_PATH
+