diff --git a/README.md b/README.md
index 15fef6d8151c72b73c5abd4102171ba8990015f1..e10f0fc39a1f3fce046ec1f00cf533ef270c71a9 100644
--- a/README.md
+++ b/README.md
@@ -159,7 +159,8 @@ Then you can do:
     _build/prefix/bin/pressure-vessel-build-relocatable-install \
         --output _build/relocatable-install \
         --archive .
-    ./tests/relocatable-install.py _build/relocatable-install  # optional
+    ./tests/pressure-vessel/relocatable-install.py \
+        _build/relocatable-install      # optional
 
 When building a development version of `pressure-vessel` using a 'scout'
 SDK Docker image, if the `pressure-vessel-build-relocatable-install`
diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml
index 67468da6156d14872712530ee7320b2cd45cf0b4..97f8db56742bc319cd534eb0d42be2a210edbee7 100644
--- a/debian/gitlab-ci.yml
+++ b/debian/gitlab-ci.yml
@@ -172,7 +172,8 @@ build:scout:
                 --archive "$(pwd)/_build" \
                 ${CI_ALLOW_MISSING_SOURCES:+--allow-missing-sources} \
                 ${NULL+}
-              prove -epython3.5 -v ./tests/relocatable-install.py :: \
+              prove -epython3.5 -v \
+                ./tests/pressure-vessel/relocatable-install.py :: \
                 "$(pwd)/_build/relocatable-install"
             fi
 
@@ -250,7 +251,7 @@ test:buster:
 
             export AUTOPKGTEST_ARTIFACTS="$(pwd)/_buster_test_logs"
             export PRESSURE_VESSEL_TEST_CONTAINERS="$(pwd)/_depot"
-            python3 tests/containers.py
+            python3 tests/pressure-vessel/containers.py
 
     artifacts:
         paths:
@@ -267,7 +268,7 @@ test:bionic:
 
             export AUTOPKGTEST_ARTIFACTS="$(pwd)/_bionic_test_logs"
             export PRESSURE_VESSEL_TEST_CONTAINERS="$(pwd)/_depot"
-            python3 tests/containers.py
+            python3 tests/pressure-vessel/containers.py
 
     artifacts:
         paths:
@@ -284,7 +285,7 @@ test:focal:
 
             export AUTOPKGTEST_ARTIFACTS="$(pwd)/_ubuntu_focal_test_logs"
             export PRESSURE_VESSEL_TEST_CONTAINERS="$(pwd)/_depot"
-            python3 tests/containers.py
+            python3 tests/pressure-vessel/containers.py
 
     artifacts:
         paths:
@@ -320,7 +321,7 @@ test:archlinux:
 
             export AUTOPKGTEST_ARTIFACTS="$(pwd)/_archlinux_test_logs"
             export PRESSURE_VESSEL_TEST_CONTAINERS="$(pwd)/_depot"
-            python3 tests/containers.py
+            python3 tests/pressure-vessel/containers.py
 
     artifacts:
         paths:
diff --git a/debian/tests/relocatable-install b/debian/tests/relocatable-install
index 4543ad76d73d739d942ecd42113cdf9b750d6e5a..c7bcd3358672694033001226b07a3ebc5f59fda7 100755
--- a/debian/tests/relocatable-install
+++ b/debian/tests/relocatable-install
@@ -26,7 +26,8 @@ else
    PYTHON=python3
 fi
 
-"$PYTHON" ./tests/relocatable-install.py "${AUTOPKGTEST_TMP}/relocatable-install"
+"$PYTHON" ./tests/pressure-vessel/relocatable-install.py \
+    "${AUTOPKGTEST_TMP}/relocatable-install"
 
 for archive in "${AUTOPKGTEST_TMP}"/*.tar.*; do
     echo "==== $(basename "$archive") ===="
diff --git a/tests/meson.build b/tests/meson.build
index 0e9a3eb2a5e665572cfa5c6eff4afecd608e96ba..ddbd1ca1096ebc8145be2742ac7444d5acff22c6 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -24,82 +24,18 @@
 test_env = environment()
 test_env.set('G_TEST_SRCDIR', meson.current_source_dir())
 test_env.set('G_TEST_BUILDDIR', meson.current_build_dir())
-test_env.set(
-  'PRESSURE_VESSEL_TEST_CONTAINERS',
-  get_option('test_containers_dir'),
-)
-test_env.set('PRESSURE_VESSEL_UNINSTALLED', 'yes')
 
 tests = [
-  'adverb.py',
-  'cheap-copy.py',
-  'containers.py',
-  'invocation.py',
-  'launcher.py',
   'mypy.sh',
   'pycodestyle.sh',
   'pyflakes.sh',
   'shellcheck.sh',
-  'test-locale-gen.sh',
-  'utils.py',
 ]
 
-compiled_tests = [
-  'bwrap-lock',
-  'resolve-in-sysroot',
-  'wait-for-child-processes',
-  'utils',
-]
-
-foreach test_name : compiled_tests
-  exe = executable(
-    'test-' + test_name,
-    files(test_name + '.c', 'test-utils.c'),
-    dependencies : [gio_unix, libglnx.get_variable('libglnx_dep')],
-    link_with : [pressure_vessel_utils],
-    include_directories : project_include_dirs,
-    install : false,
-  )
-
-  if glib_tap_support.found()
-    if prove.found()
-      test(
-        test_name, prove,
-        args : ['-v', '-e', '', exe, '::', '--tap'],
-        env : test_env,
-      )
-    else
-      test(
-        test_name, exe,
-        args : ['--tap'],
-        env : test_env,
-      )
-    endif
-  else
-    test(
-      test_name, exe,
-      env : test_env,
-    )
-  endif
-
-endforeach
-
 foreach test_name : tests
   test_args = ['-v', files(test_name)]
   timeout = 30
 
-  if test_name.endswith('.py')
-    test_args += ['-e', python.path()]
-  endif
-
-  if test_name.endswith('launcher.py')
-    timeout = 60
-  endif
-
-  if test_name.endswith('containers.py')
-    timeout = 300
-  endif
-
   if prove.found()
     if dbus_run_session.found()
       test(
@@ -108,23 +44,6 @@ foreach test_name : tests
         env : test_env,
         timeout : timeout,
       )
-
-      # Re-run launcher.py with the default python3 version (if any)
-      # to try the code path involving gi.repository.Gio
-      if test_name == 'launcher.py' and full_python.found()
-        test(
-          'full-python-' + test_name,
-          dbus_run_session,
-          args : [
-            '--',
-            'prove',
-            '-v', files(test_name),
-            '-e', full_python.path(),
-          ],
-          env : test_env,
-          timeout : timeout,
-        )
-      endif
     else
       test(
         test_name, prove,
@@ -136,29 +55,6 @@ foreach test_name : tests
   endif
 endforeach
 
-# Helper programs and manual tests
-helpers = [
-  'cheap-copy',
-  'elf-get-soname',
-  'helper',
-]
-
-foreach helper : helpers
-  executable(
-    'test-' + helper,
-    sources : [
-      helper + '.c',
-    ],
-    dependencies : [
-      gio_unix,
-      libglnx.get_variable('libglnx_dep'),
-    ],
-    link_with : [
-      pressure_vessel_utils,
-    ],
-    include_directories : project_include_dirs,
-    install : false,
-  )
-endforeach
+subdir('pressure-vessel')
 
 # vim:set sw=2 sts=2 et:
diff --git a/tests/mypy.sh b/tests/mypy.sh
index 84cb893c472b2b64d0fceb770982070e19bcdbe7..e3a077758d5d33db7432d8e43079af47b0510078 100755
--- a/tests/mypy.sh
+++ b/tests/mypy.sh
@@ -40,7 +40,7 @@ for script in \
     ./pressure-vessel/*.py \
     ./pressure-vessel/pressure-vessel-test-ui \
     ./sysroot/*.py \
-    ./tests/*.py \
+    ./tests/*/*.py \
 ; do
     i=$((i + 1))
     if [ "x${MYPY:="$(command -v mypy || echo false)"}" = xfalse ]; then
diff --git a/tests/adverb.py b/tests/pressure-vessel/adverb.py
similarity index 100%
rename from tests/adverb.py
rename to tests/pressure-vessel/adverb.py
diff --git a/tests/bwrap-lock.c b/tests/pressure-vessel/bwrap-lock.c
similarity index 100%
rename from tests/bwrap-lock.c
rename to tests/pressure-vessel/bwrap-lock.c
diff --git a/tests/cheap-copy.c b/tests/pressure-vessel/cheap-copy.c
similarity index 100%
rename from tests/cheap-copy.c
rename to tests/pressure-vessel/cheap-copy.c
diff --git a/tests/cheap-copy.py b/tests/pressure-vessel/cheap-copy.py
similarity index 100%
rename from tests/cheap-copy.py
rename to tests/pressure-vessel/cheap-copy.py
diff --git a/tests/containers.py b/tests/pressure-vessel/containers.py
similarity index 99%
rename from tests/containers.py
rename to tests/pressure-vessel/containers.py
index 6f2f2a9b2f774d0417a63cb7c51ec11af9502cdb..681108352e75942d56c140dcd881bdcf4443b8e5 100755
--- a/tests/containers.py
+++ b/tests/pressure-vessel/containers.py
@@ -47,12 +47,12 @@ that was built against scout:
         --check-source-directory="$PRESSURE_VESSEL_TEST_CONTAINERS" \
         --allow-missing-sources \
         --srcdir="$(pwd)" --set-version "$(git describe)"
-    ./tests/containers.py
+    ./tests/pressure-vessel/containers.py
 
 or against the SteamLinuxRuntime depot that gets uploaded to the Steam CDN:
 
     export PRESSURE_VESSEL_TEST_CONTAINERS=../SteamLinuxRuntime/depot
-    ./tests/containers.py
+    ./tests/pressure-vessel/containers.py
 
 Influential environment variables:
 
@@ -63,9 +63,11 @@ Influential environment variables:
 * BWRAP:
     A bubblewrap executable
 * G_TEST_SRCDIR:
-    The ./tests subdirectory of the source root, typically $(pwd)/tests
+    The ./tests/pressure-vessel subdirectory of the source root,
+    typically $(pwd)/tests/pressure-vessel
 * G_TEST_BUILDDIR:
-    The ./tests subdirectory of the build root, typically $(pwd)/_build/tests
+    The ./tests/pressure-vessel subdirectory of the build root,
+    typically $(pwd)/_build/tests/pressure-vessel
 * PRESSURE_VESSEL_LIBCAPSULE_TOOLS:
     Override the location of capsule-capture-libs etc.
 * PRESSURE_VESSEL_TEST_CONTAINERS:
diff --git a/tests/elf-get-soname.c b/tests/pressure-vessel/elf-get-soname.c
similarity index 100%
rename from tests/elf-get-soname.c
rename to tests/pressure-vessel/elf-get-soname.c
diff --git a/tests/helper.c b/tests/pressure-vessel/helper.c
similarity index 100%
rename from tests/helper.c
rename to tests/pressure-vessel/helper.c
diff --git a/tests/inside-runtime.py b/tests/pressure-vessel/inside-runtime.py
similarity index 100%
rename from tests/inside-runtime.py
rename to tests/pressure-vessel/inside-runtime.py
diff --git a/tests/invocation.py b/tests/pressure-vessel/invocation.py
similarity index 100%
rename from tests/invocation.py
rename to tests/pressure-vessel/invocation.py
diff --git a/tests/launcher.py b/tests/pressure-vessel/launcher.py
similarity index 100%
rename from tests/launcher.py
rename to tests/pressure-vessel/launcher.py
diff --git a/tests/pressure-vessel/meson.build b/tests/pressure-vessel/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..c1d1f66eebf688efa78ea1b4ce39a6a38f15f00c
--- /dev/null
+++ b/tests/pressure-vessel/meson.build
@@ -0,0 +1,160 @@
+# Copyright © 2019-2020 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+test_env = environment()
+test_env.set('G_TEST_SRCDIR', meson.current_source_dir())
+test_env.set('G_TEST_BUILDDIR', meson.current_build_dir())
+test_env.set(
+  'PRESSURE_VESSEL_TEST_CONTAINERS',
+  get_option('test_containers_dir'),
+)
+test_env.set('PRESSURE_VESSEL_UNINSTALLED', 'yes')
+
+tests = [
+  'adverb.py',
+  'cheap-copy.py',
+  'containers.py',
+  'invocation.py',
+  'launcher.py',
+  'test-locale-gen.sh',
+  'utils.py',
+]
+
+compiled_tests = [
+  'bwrap-lock',
+  'resolve-in-sysroot',
+  'wait-for-child-processes',
+  'utils',
+]
+
+foreach test_name : compiled_tests
+  exe = executable(
+    'test-' + test_name,
+    files(test_name + '.c', 'test-utils.c'),
+    dependencies : [gio_unix, libglnx.get_variable('libglnx_dep')],
+    link_with : [pressure_vessel_utils],
+    include_directories : project_include_dirs,
+    install : false,
+  )
+
+  if glib_tap_support.found()
+    if prove.found()
+      test(
+        test_name, prove,
+        args : ['-v', '-e', '', exe, '::', '--tap'],
+        env : test_env,
+      )
+    else
+      test(
+        test_name, exe,
+        args : ['--tap'],
+        env : test_env,
+      )
+    endif
+  else
+    test(
+      test_name, exe,
+      env : test_env,
+    )
+  endif
+
+endforeach
+
+foreach test_name : tests
+  test_args = ['-v', files(test_name)]
+  timeout = 30
+
+  if test_name.endswith('.py')
+    test_args += ['-e', python.path()]
+  endif
+
+  if test_name.endswith('launcher.py')
+    timeout = 60
+  endif
+
+  if test_name.endswith('containers.py')
+    timeout = 300
+  endif
+
+  if prove.found()
+    if dbus_run_session.found()
+      test(
+        test_name, dbus_run_session,
+        args : ['--', 'prove'] + test_args,
+        env : test_env,
+        timeout : timeout,
+      )
+
+      # Re-run launcher.py with the default python3 version (if any)
+      # to try the code path involving gi.repository.Gio
+      if test_name == 'launcher.py' and full_python.found()
+        test(
+          'full-python-' + test_name,
+          dbus_run_session,
+          args : [
+            '--',
+            'prove',
+            '-v', files(test_name),
+            '-e', full_python.path(),
+          ],
+          env : test_env,
+          timeout : timeout,
+        )
+      endif
+    else
+      test(
+        test_name, prove,
+        args : test_args,
+        env : test_env,
+        timeout : timeout,
+      )
+    endif
+  endif
+endforeach
+
+# Helper programs and manual tests
+helpers = [
+  'cheap-copy',
+  'elf-get-soname',
+  'helper',
+]
+
+foreach helper : helpers
+  executable(
+    'test-' + helper,
+    sources : [
+      helper + '.c',
+    ],
+    dependencies : [
+      gio_unix,
+      libglnx.get_variable('libglnx_dep'),
+    ],
+    link_with : [
+      pressure_vessel_utils,
+    ],
+    include_directories : project_include_dirs,
+    install : false,
+  )
+endforeach
+
+# vim:set sw=2 sts=2 et:
diff --git a/tests/pressure-vessel.supp b/tests/pressure-vessel/pressure-vessel.supp
similarity index 100%
rename from tests/pressure-vessel.supp
rename to tests/pressure-vessel/pressure-vessel.supp
diff --git a/tests/relocatable-install.py b/tests/pressure-vessel/relocatable-install.py
similarity index 100%
rename from tests/relocatable-install.py
rename to tests/pressure-vessel/relocatable-install.py
diff --git a/tests/resolve-in-sysroot.c b/tests/pressure-vessel/resolve-in-sysroot.c
similarity index 100%
rename from tests/resolve-in-sysroot.c
rename to tests/pressure-vessel/resolve-in-sysroot.c
diff --git a/tests/test-locale-gen.sh b/tests/pressure-vessel/test-locale-gen.sh
similarity index 98%
rename from tests/test-locale-gen.sh
rename to tests/pressure-vessel/test-locale-gen.sh
index ddecb1c9b58d55374b871e45b60419d30d4494eb..f26613bc8528ee48bd4bc71eedab3b19e3d37b0c 100755
--- a/tests/test-locale-gen.sh
+++ b/tests/pressure-vessel/test-locale-gen.sh
@@ -82,7 +82,7 @@ if [ -z "${G_TEST_BUILDDIR-}" ]; then
 fi
 
 if [ -n "${PRESSURE_VESSEL_UNINSTALLED-}" ]; then
-    PATH="${G_TEST_BUILDDIR}/../pressure-vessel:${G_TEST_SRCDIR}/../pressure-vessel:${PATH}"
+    PATH="${G_TEST_BUILDDIR}/../../pressure-vessel:${G_TEST_SRCDIR}/../../pressure-vessel:${PATH}"
 fi
 
 locale_gen="pressure-vessel-locale-gen"
diff --git a/tests/test-utils.c b/tests/pressure-vessel/test-utils.c
similarity index 100%
rename from tests/test-utils.c
rename to tests/pressure-vessel/test-utils.c
diff --git a/tests/test-utils.h b/tests/pressure-vessel/test-utils.h
similarity index 100%
rename from tests/test-utils.h
rename to tests/pressure-vessel/test-utils.h
diff --git a/tests/testutils.py b/tests/pressure-vessel/testutils.py
similarity index 92%
rename from tests/testutils.py
rename to tests/pressure-vessel/testutils.py
index 010ea409baf09c487df953068558ab16ac1d99bd..0c13fce3be6e38c75faccb4d3e913d65d4eeaaf3 100644
--- a/tests/testutils.py
+++ b/tests/pressure-vessel/testutils.py
@@ -93,14 +93,16 @@ class BaseTest(unittest.TestCase):
             'G_TEST_SRCDIR',
             os.path.abspath(os.path.dirname(__file__)),
         )
-        cls.top_srcdir = os.path.dirname(cls.G_TEST_SRCDIR)
+        cls.top_srcdir = os.path.dirname(os.path.dirname(cls.G_TEST_SRCDIR))
         cls.G_TEST_BUILDDIR = os.getenv(
             'G_TEST_BUILDDIR',
             os.path.abspath(
-                os.path.join(os.path.dirname(__file__), '..', '_build'),
+                os.path.join(os.path.dirname(__file__), '..', '..', '_build'),
             ),
         )
-        cls.top_builddir = os.path.dirname(cls.G_TEST_BUILDDIR)
+        cls.top_builddir = os.path.dirname(
+            os.path.dirname(cls.G_TEST_BUILDDIR)
+        )
 
         cls.tmpdir = tempfile.TemporaryDirectory()
 
@@ -136,7 +138,10 @@ class BaseTest(unittest.TestCase):
             ]
 
             for supp in (
-                os.path.join(self.top_srcdir, 'tests', 'pressure-vessel.supp'),
+                os.path.join(
+                    self.top_srcdir, 'tests', 'pressure-vessel',
+                    'pressure-vessel.supp',
+                ),
                 '/usr/share/glib-2.0/valgrind/glib.supp',
             ):
                 if os.path.isfile(supp):
diff --git a/tests/utils.c b/tests/pressure-vessel/utils.c
similarity index 100%
rename from tests/utils.c
rename to tests/pressure-vessel/utils.c
diff --git a/tests/utils.py b/tests/pressure-vessel/utils.py
similarity index 100%
rename from tests/utils.py
rename to tests/pressure-vessel/utils.py
diff --git a/tests/wait-for-child-processes.c b/tests/pressure-vessel/wait-for-child-processes.c
similarity index 100%
rename from tests/wait-for-child-processes.c
rename to tests/pressure-vessel/wait-for-child-processes.c
diff --git a/tests/pycodestyle.sh b/tests/pycodestyle.sh
index e204376511d28318d3b6c3015b6048dce831e551..41809b2d113415672f480478a60c63d65d7c2a3f 100755
--- a/tests/pycodestyle.sh
+++ b/tests/pycodestyle.sh
@@ -50,7 +50,7 @@ if "${PYCODESTYLE}" \
     ./pressure-vessel/*.py \
     ./pressure-vessel/pressure-vessel-test-ui \
     ./sysroot/*.py \
-    ./tests/*.py \
+    ./tests/*/*.py \
     >&2; then
     echo "ok 1 - $PYCODESTYLE reported no issues"
 else
diff --git a/tests/pyflakes.sh b/tests/pyflakes.sh
index 715dbce35493a5eb42a7b717d3d2e7f771d0a957..9ce8b2a14c0225ec5218d0f4546aaf6c5ff7cc9d 100755
--- a/tests/pyflakes.sh
+++ b/tests/pyflakes.sh
@@ -40,7 +40,7 @@ elif "${PYFLAKES}" \
     ./pressure-vessel/*.py \
     ./pressure-vessel/pressure-vessel-test-ui \
     ./sysroot/*.py \
-    ./tests/*.py \
+    ./tests/*/*.py \
     >&2; then
     echo "1..1"
     echo "ok 1 - $PYFLAKES reported no issues"
diff --git a/tests/shellcheck.sh b/tests/shellcheck.sh
index 9a5daa2fe8c50773865ba31372d5288f72e96988..beed72bdbc06d0dba0089737f1d509baaa764cfd 100755
--- a/tests/shellcheck.sh
+++ b/tests/shellcheck.sh
@@ -38,6 +38,7 @@ for shell_script in \
         ./pressure-vessel/pressure-vessel-locale-gen \
         ./pressure-vessel/pressure-vessel-unruntime \
         ./tests/*.sh \
+        ./tests/*/*.sh \
         ; do
     n=$((n + 1))