diff --git a/ci/Jenkinsfile b/ci/Jenkinsfile
index 270f34e91c7bdcafd79b84f966b551f8daf2e30a..e396c50f90a27bc30a43a9417493d6d984eacaa6 100644
--- a/ci/Jenkinsfile
+++ b/ci/Jenkinsfile
@@ -70,18 +70,6 @@ pipeline {
               '''
             }
           }
-
-          /* Check for self-containedness by repeating relocatable-install.sh
-           * in a somewhat minimal container. */
-          docker.image('debian:buster-slim').inside() {
-            sh '''
-            set -eu
-            export G_TEST_SRCDIR="${WORKSPACE}/src"
-            export G_TEST_BUILDDIR="${WORKSPACE}/src/_build"
-            cd src
-            ./t/relocatable-install.sh
-            '''
-          }
         }
 
         dir('src/_build') {
diff --git a/meson.build b/meson.build
index 8b1a24ca653e7a076d5374c190b756ce4fe01310..c3b077f985c98748d073bf0a6db7aa5769b3646a 100644
--- a/meson.build
+++ b/meson.build
@@ -45,11 +45,11 @@ scripts = [
 ]
 
 tests = [
-  'mypy',
-  'pycodestyle',
-  'pyflakes',
-  'relocatable-install',
-  'shellcheck',
+  'mypy.sh',
+  'pycodestyle.sh',
+  'pyflakes.sh',
+  'relocatable-install.py',
+  'shellcheck.sh',
 ]
 
 test_env = environment()
@@ -60,7 +60,7 @@ foreach test_name : tests
   if prove.found()
     test(
       test_name, prove,
-      args : ['-v', files('t/' + test_name + '.sh')],
+      args : ['-v', files('t/' + test_name)],
       env : test_env,
     )
   endif
diff --git a/t/mypy.sh b/t/mypy.sh
index b552359755e1ef2deba9ab26d067d523c7cb0a24..6dd270a7176661ee25eb96579e11e7b63fc5b503 100755
--- a/t/mypy.sh
+++ b/t/mypy.sh
@@ -23,6 +23,7 @@ for script in \
     "${G_TEST_SRCDIR}"/*.py \
     "${G_TEST_SRCDIR}"/pressure-vessel-test-ui \
     "${G_TEST_SRCDIR}"/sysroot/*.py \
+    "${G_TEST_SRCDIR}"/t/*.py \
 ; do
     i=$((i + 1))
     if [ "x${MYPY:="$(command -v mypy || echo false)"}" = xfalse ]; then
diff --git a/t/pycodestyle.sh b/t/pycodestyle.sh
index 0c0113a1b8bcda8b52d895c2f8d41afca1a05c0f..a26f9e5c68511037491f216df2e7ae9c71659312 100755
--- a/t/pycodestyle.sh
+++ b/t/pycodestyle.sh
@@ -26,12 +26,14 @@ echo "1..1"
 
 # Ignore E402: when using GObject-Introspection, not all imports
 # can come first
+# Ignore W503: allow wrapping long expressions before a binary operator
 
 if "${PYCODESTYLE}" \
-    --ignore=E402 \
+    --ignore=E402,W503 \
     "$G_TEST_SRCDIR"/*.py \
     "${G_TEST_SRCDIR}"/pressure-vessel-test-ui \
     "${G_TEST_SRCDIR}"/sysroot/*.py \
+    "${G_TEST_SRCDIR}"/t/*.py \
     >&2; then
     echo "ok 1 - $PYCODESTYLE reported no issues"
 else
diff --git a/t/pyflakes.sh b/t/pyflakes.sh
index 5134e76448382cd6dc807a3198a976cc062a7f32..5f7f497c8e4028c6c3e49d147103a410f04ccdcb 100755
--- a/t/pyflakes.sh
+++ b/t/pyflakes.sh
@@ -23,6 +23,7 @@ elif "${PYFLAKES}" \
     "${G_TEST_SRCDIR}"/*.py \
     "${G_TEST_SRCDIR}"/pressure-vessel-test-ui \
     "${G_TEST_SRCDIR}"/sysroot/*.py \
+    "${G_TEST_SRCDIR}"/t/*.py \
     >&2; then
     echo "1..1"
     echo "ok 1 - $PYFLAKES reported no issues"
diff --git a/t/relocatable-install.py b/t/relocatable-install.py
new file mode 100755
index 0000000000000000000000000000000000000000..4c06e4615f33db162f0a4d50dab46de18a4aca9a
--- /dev/null
+++ b/t/relocatable-install.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+# Copyright 2018-2019 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+
+import os
+import subprocess
+import sys
+
+try:
+    import typing
+    typing      # silence pyflakes
+except ImportError:
+    pass
+
+
+G_TEST_SRCDIR = os.getenv(
+    'G_TEST_SRCDIR',
+    os.path.abspath(
+        os.path.join(os.path.dirname(__file__), os.pardir),
+    ),
+)
+G_TEST_BUILDDIR = os.getenv('G_TEST_BUILDDIR', os.path.abspath('_build'))
+
+
+class TapTest:
+    def __init__(self):
+        # type: () -> None
+        self.test_num = 0
+        self.failed = False
+
+    def ok(self, desc):
+        # type: (str) -> None
+        self.test_num += 1
+        print('ok %d - %s' % (self.test_num, desc))
+
+    def skip(self, desc):
+        # type: (str) -> None
+        self.test_num += 1
+        print('ok %d # SKIP %s' % (self.test_num, desc))
+
+    def diag(self, text):
+        # type: (str) -> None
+        print('# %s' % text)
+
+    def not_ok(self, desc):
+        # type: (str) -> None
+        self.test_num += 1
+        print('not ok %d - %s' % (self.test_num, desc))
+        self.failed = True
+
+    def done_testing(self):
+        # type: () -> None
+        print('1..%d' % self.test_num)
+
+        if self.failed:
+            sys.exit(1)
+
+
+EXES = [
+    'pressure-vessel-wrap-c',
+]
+WRAPPED = [
+    'bwrap',
+]
+MULTIARCH = [
+    'capsule-capture-libs',
+    'capsule-symbols',
+]
+SCRIPTS = [
+    'pressure-vessel-unruntime',
+    'pressure-vessel-wrap',
+]
+LD_SO = [
+    ('x86_64-linux-gnu', '/lib64/ld-linux-x86-64.so.2'),
+    ('i386-linux-gnu', '/lib/ld-linux.so.2'),
+]
+
+
+def isexec(path):
+    # type: (str) -> bool
+    try:
+        return (os.stat(path).st_mode & 0o111) != 0
+    except OSError:
+        return False
+
+
+def main():
+    # type: () -> None
+    relocatable_install = os.path.join(G_TEST_BUILDDIR, 'relocatable-install')
+
+    if not os.path.exists(relocatable_install):
+        print('1..0 # SKIP relocatable-install not in expected location')
+        sys.exit(0)
+
+    test = TapTest()
+
+    for exe in EXES + WRAPPED:
+        path = os.path.join(relocatable_install, 'bin', exe)
+
+        if subprocess.call([path, '--help'], stdout=2) == 0:
+            test.ok('{} --help'.format(path))
+        else:
+            test.not_ok('{} --help'.format(path))
+
+    for exe in EXES:
+        path = os.path.join(relocatable_install, 'bin', exe)
+
+    for exe in WRAPPED:
+        path = os.path.join(relocatable_install, 'bin', exe)
+
+    for basename in MULTIARCH:
+        for multiarch, ld_so in LD_SO:
+            exe = '{}-{}'.format(multiarch, basename)
+            path = os.path.join(relocatable_install, 'bin', exe)
+
+            if isexec(path):
+                test.ok('{} exists and is executable'.format(path))
+            else:
+                test.not_ok('{} not executable'.format(path))
+
+            if not isexec(ld_so):
+                test.skip('{} not found'.format(ld_so))
+            elif basename == 'capsule-symbols':
+                test.skip('capsule-symbols has no --help yet')
+            elif subprocess.call([path, '--help'], stdout=2) == 0:
+                test.ok('{} --help'.format(path))
+            else:
+                test.not_ok('{} --help'.format(path))
+
+    for exe in SCRIPTS:
+        path = os.path.join(relocatable_install, 'bin', exe)
+        if not isexec('/bin/bash'):
+            test.skip('bash not found')
+        elif subprocess.call([path, '--help'], stdout=2) == 0:
+            test.ok('{} --help'.format(path))
+        else:
+            test.not_ok('{} --help'.format(path))
+
+    test.done_testing()
+
+
+if __name__ == '__main__':
+    main()
+
+# vi: set sw=4 sts=4 et:
diff --git a/t/relocatable-install.sh b/t/relocatable-install.sh
deleted file mode 100755
index 9d9c1f571a76a06ade87ee7ee446cd11e4715cb0..0000000000000000000000000000000000000000
--- a/t/relocatable-install.sh
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/bin/sh
-set -eu
-
-if [ -z "${G_TEST_SRCDIR-}" ]; then
-    me="$(readlink -f "$0")"
-    srcdir="${me%/*}"
-    G_TEST_SRCDIR="${srcdir%/*}"
-fi
-
-: "${G_TEST_BUILDDIR:=.}"
-
-n=0
-failed=
-
-EXES="
-bwrap
-"
-
-MULTIARCH="
-capsule-capture-libs
-capsule-symbols
-"
-
-SCRIPTS="
-pressure-vessel-wrap
-pressure-vessel-unruntime
-"
-
-relocatable_install="${G_TEST_BUILDDIR}/relocatable-install"
-
-if ! [ -e "${relocatable_install}" ]; then
-    echo "1..0 # SKIP relocatable-install not in expected location"
-    exit 0
-fi
-
-for exe in $EXES; do
-    n=$(( n + 1 ))
-
-    if "${relocatable_install}/bin/$exe" --help >&2; then
-        echo "ok $n - $exe --help"
-    else
-        echo "not ok $n - $exe --help"
-        failed=yes
-    fi
-done
-
-for basename in $MULTIARCH; do
-    for pair in \
-        x86_64-linux-gnu:/lib64/ld-linux-x86-64.so.2 \
-        i386-linux-gnu:/lib/ld-linux.so.2 \
-    ; do
-        ld_so="${pair#*:}"
-        multiarch="${pair%:*}"
-        exe="${multiarch}-${basename}"
-
-        n=$(( n + 1 ))
-
-        if [ -x "${relocatable_install}/bin/$exe" ]; then
-            echo "ok $n - $exe exists and is executable"
-        else
-            echo "not ok $n - $exe not executable"
-            failed=yes
-        fi
-
-        n=$(( n + 1 ))
-
-        if ! [ -x "$ld_so" ]; then
-            echo "ok $n - $exe # SKIP: $ld_so not found"
-        elif [ "$basename" = "capsule-symbols" ]; then
-            echo "ok $n - $exe # SKIP: capsule-symbols has no --help yet"
-        elif "${relocatable_install}/bin/$exe" --help >&2; then
-            echo "ok $n - $exe --help"
-        else
-            echo "not ok $n - $exe --help"
-            failed=yes
-        fi
-    done
-done
-
-for exe in $SCRIPTS; do
-    n=$(( n + 1 ))
-
-    if ! [ -x /bin/bash ]; then
-        echo "ok $n - $exe # SKIP Cannot run a bash script without bash"
-    elif "${relocatable_install}/bin/$exe" --help >&2; then
-        echo "ok $n - $exe --help"
-    else
-        echo "not ok $n - $exe --help"
-        failed=yes
-    fi
-done
-
-echo "1..$n"
-
-if [ -n "$failed" ]; then
-    exit 1
-fi
-
-# vim:set sw=4 sts=4 et: