diff --git a/tests/.mypy.ini b/tests/.mypy.ini
index aad8c0c90..07afc4a11 100644
--- a/tests/.mypy.ini
+++ b/tests/.mypy.ini
@@ -4,7 +4,7 @@ check_untyped_defs=True
files=.
exclude = (?x)(
templates/.*.py # template files are a bit special
- |test-document-fuse.py$ # has issues with typing
+ |test_document_fuse.py$ # has issues with typing
)
[mypy-gi.*]
diff --git a/tests/README.md b/tests/README.md
index dfa23a933..a0ac5b19d 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -15,6 +15,9 @@ but should not normally be set on production systems:
reliable are skipped.
Set this for automated QA testing, leave it unset during development.
+* `XDP_TEST_RUN_LONG`: If set (to any value), some tests will run more
+ iterations or otherwise test more thoroughly.
+
* `XDP_VALIDATE_ICON_INSECURE`: If set (to any value), x-d-p doesn't
sandbox the icon validator using **bwrap**(1), even if sandboxed
validation was enabled at compile time.
diff --git a/tests/__init__.py b/tests/__init__.py
index a948f530e..84d229592 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -39,6 +39,10 @@ def is_in_container() -> bool:
)
+def run_long_tests() -> bool:
+ return os.environ.get("XDP_TEST_RUN_LONG") is not None
+
+
def wait(ms: int):
"""
Waits for the specified amount of milliseconds.
diff --git a/tests/meson.build b/tests/meson.build
index 4eac2788b..1e042e758 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -212,15 +212,6 @@ foreach p : portal_limited
)
endforeach
-if enable_installed_tests
- install_data(
- 'session.conf.in',
- 'test-document-fuse.sh',
- 'test-document-fuse.py',
- install_dir: installed_tests_dir
- )
-endif
-
test_permission_store = executable(
'test-permission-store',
'test-permission-store.c',
@@ -315,6 +306,7 @@ if enable_pytest
'test_background.py',
'test_camera.py',
'test_clipboard.py',
+ 'test_document_fuse.py',
'test_email.py',
'test_filechooser.py',
'test_globalshortcuts.py',
@@ -356,7 +348,6 @@ if enable_installed_tests
testfiles = [
'testdb',
'test-doc-portal',
- 'test-document-fuse.sh',
'test-permission-store',
'test-xdp-utils',
]
diff --git a/tests/test-document-fuse.sh b/tests/test-document-fuse.sh
deleted file mode 100755
index 6552c48b5..000000000
--- a/tests/test-document-fuse.sh
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/bin/bash
-
-
-skip() {
- echo "1..0 # SKIP" "$@"
- exit 0
-}
-
-skip_without_fuse () {
- fusermount3 --version >/dev/null 2>&1 || skip "no fusermount3"
-
- capsh --print | grep -q 'Bounding set.*[^a-z]cap_sys_admin' || \
- skip "No cap_sys_admin in bounding set, can't use FUSE"
-
- [ -w /dev/fuse ] || skip "no write access to /dev/fuse"
- [ -e /etc/mtab ] || skip "no /etc/mtab"
-}
-
-skip_without_fuse
-
-echo "1..2"
-
-set -e
-
-if [ -n "${G_TEST_SRCDIR:-}" ]; then
- test_srcdir="${G_TEST_SRCDIR}"
-else
- test_srcdir=$(realpath "$(dirname $0)")
-fi
-
-if [ -n "${G_TEST_BUILDDIR:-}" ]; then
- test_builddir="${G_TEST_BUILDDIR}"
-else
- test_builddir=$(realpath "$(dirname $0)")
-fi
-
-export TEST_DATA_DIR=`mktemp -d /tmp/xdp-XXXXXX`
-mkdir -p "${TEST_DATA_DIR}/home"
-mkdir -p "${TEST_DATA_DIR}/runtime"
-mkdir -p "${TEST_DATA_DIR}/system"
-mkdir -p "${TEST_DATA_DIR}/config"
-
-export HOME=${TEST_DATA_DIR}/home
-export XDG_CACHE_HOME=${TEST_DATA_DIR}/home/cache
-export XDG_CONFIG_HOME=${TEST_DATA_DIR}/home/config
-export XDG_DATA_HOME=${TEST_DATA_DIR}/home/share
-export XDG_RUNTIME_DIR=${TEST_DATA_DIR}/runtime
-
-cleanup () {
- fusermount3 -u "$XDG_RUNTIME_DIR/doc" || :
- sleep 0.1
- kill "$DBUS_SESSION_BUS_PID"
- kill $(jobs -p) &> /dev/null || true
- rm -rf "$TEST_DATA_DIR"
-}
-trap cleanup EXIT
-
-ITERATIONS=3
-PARALLEL_TESTS=20
-PARALLEL_ITERATIONS=10
-
-if [ -n "$XDP_TEST_IN_CI" ]; then
- PARALLEL_TESTS=10
- PARALLEL_ITERATIONS=5
-fi
-
-sed "s#@testdir@#${test_builddir}#" "${test_srcdir}/session.conf.in" > session.conf
-
-dbus-daemon --fork --config-file=session.conf --print-address=3 --print-pid=4 \
- 3> dbus-session-bus-address 4> dbus-session-bus-pid
-export DBUS_SESSION_BUS_ADDRESS="$(cat dbus-session-bus-address)"
-DBUS_SESSION_BUS_PID="$(cat dbus-session-bus-pid)"
-
-if ! kill -0 "$DBUS_SESSION_BUS_PID"; then
- assert_not_reached "Failed to start dbus-daemon"
-fi
-
-# Run portal manually so that we get any segfault our assert output
-# Add -v here to get debug output from fuse
-# Only do this when running uninstalled; when running as an installed-test,
-# we rely on D-Bus activation.
-if [ -n "${XDP_UNINSTALLED:-}" ]; then
- $test_builddir/../document-portal/xdg-document-portal -r &
- sleep 0.2 # Make sure the portal has connected to dbus
-fi
-
-# First run a basic single-thread test
-echo Testing single-threaded >&2
-"${test_srcdir}/test-document-fuse.py" --iterations ${ITERATIONS} -v
-echo "ok single-threaded"
-
-# Then a bunch of copies in parallel to stress-test
-echo Testing in parallel >&2
-PIDS=()
-for i in $(seq ${PARALLEL_TESTS}); do
- "${test_srcdir}/test-document-fuse.py" --iterations ${PARALLEL_ITERATIONS} --prefix "$i" &
- PID="$!"
- PIDS+=( "$PID" )
-done
-
-echo waiting for pids "${PIDS[@]}" >&2
-wait "${PIDS[@]}"
-echo "ok load-test"
diff --git a/tests/test-document-fuse.py b/tests/test_document_fuse.py
old mode 100755
new mode 100644
similarity index 93%
rename from tests/test-document-fuse.py
rename to tests/test_document_fuse.py
index 47695ed22..29f9f2628
--- a/tests/test-document-fuse.py
+++ b/tests/test_document_fuse.py
@@ -1,69 +1,43 @@
-#!/usr/bin/env python3
-
-# Copyright © 2020 Red Hat, Inc
-# Copyright © 2023 GNOME Foundation Inc.
-#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see .
-#
-# Authors:
-# Alexander Larsson
-# Hubert Figuière
+# This file is formatted with Python Black
-import argparse
+import tests as xdp
+
+import pytest
import errno
import os
import random
import stat
import sys
-
+import multiprocessing as mp
+import traceback
+import subprocess
from gi.repository import Gio, GLib
+@pytest.fixture
+def app_id():
+ return None
+
+
def filename_to_ay(filename):
return list(filename.encode("utf-8")) + [0]
-running_count = {}
-
app_prefix = "org.test."
dir_prefix = "dir"
ensure_no_remaining = True
-parser = argparse.ArgumentParser()
-parser.add_argument("--verbose", "-v", action="count")
-parser.add_argument("--iterations", type=int, default=3)
-parser.add_argument("--prefix")
-args = parser.parse_args(sys.argv[1:])
-
-if args.prefix:
- app_prefix = app_prefix + args.prefix + "."
- dir_prefix = dir_prefix + "-" + args.prefix + "-"
- ensure_no_remaining = False
+running_count = {}
def log(str):
- if args.prefix:
- print("%s: %s" % (args.prefix, str), file=sys.stderr)
- else:
- print(str, file=sys.stderr)
+ print(str, file=sys.stderr)
def logv(str):
- if args.verbose:
- log(str)
+ log(str)
def get_a_count(counter):
@@ -103,7 +77,6 @@ def appendFdContent(fd, content):
os.write(fd, bytes(content, "utf-8"))
-TEST_DATA_DIR = os.environ["TEST_DATA_DIR"]
DOCUMENT_ADD_FLAGS_REUSE_EXISTING = 1 << 0
DOCUMENT_ADD_FLAGS_PERSISTENT = 1 << 1
DOCUMENT_ADD_FLAGS_AS_NEEDED_BY_APP = 1 << 2
@@ -1010,7 +983,7 @@ def create_app_by_lookup(portal):
def ensure_real_dir(create_hidden_file=True):
count = get_a_count("doc")
- dir = TEST_DATA_DIR + "/" + dir_prefix + str(count)
+ dir = os.environ["TMPDIR"] + "/" + dir_prefix + str(count)
os.makedirs(dir)
if create_hidden_file:
setFileContent(dir + "/cant-see-this-file", "s3krit")
@@ -1208,7 +1181,17 @@ def file_transfer_portal_test():
log("File transfer tests ok")
-try:
+def run_test(iterations, prefix=None, do_ensure_no_remaining=True):
+ global app_prefix
+ global dir_prefix
+ global ensure_no_remaining
+
+ if prefix:
+ app_prefix = app_prefix + prefix + "."
+ dir_prefix = dir_prefix + "-" + prefix + "-"
+
+ ensure_no_remaining = do_ensure_no_remaining
+
log("Connecting to portal")
doc_portal = DocPortal()
@@ -1241,7 +1224,7 @@ def file_transfer_portal_test():
add_an_app(doc_portal, 6)
verify_fs_layout(doc_portal)
- for i in range(args.iterations):
+ for i in range(iterations):
log("Checking permissions, pass %d" % (i + 1))
check_perms(doc_portal)
verify_fs_layout(doc_portal)
@@ -1249,7 +1232,74 @@ def file_transfer_portal_test():
log("fuse tests ok")
file_transfer_portal_test()
- sys.exit(0)
-except Exception as e:
- log("fuse tests failed: %s" % e)
- sys.exit(1)
+
+class Process(mp.Process):
+ def __init__(self, *args, **kwargs):
+ mp.Process.__init__(self, *args, **kwargs)
+ self._pconn, self._cconn = mp.Pipe()
+ self._exception = None
+
+ def run(self):
+ try:
+ mp.Process.run(self)
+ self._cconn.send(None)
+ except Exception as e:
+ tb = traceback.format_exc()
+ self._cconn.send((e, tb))
+
+ @property
+ def exception(self):
+ if self._pconn.poll():
+ self._exception = self._pconn.recv()
+ return self._exception
+
+
+class TestDocumentFuse:
+ def parallel(self, test_function, parallel_tests, parallel_iterations):
+ procs = []
+ for i in range(parallel_tests):
+ p = Process(
+ target=test_function, args=(parallel_iterations, f"c{i}", False)
+ )
+ p.start()
+ procs.append(p)
+
+ for p in procs:
+ p.join()
+
+ if p.exception:
+ error, traceback = p.exception
+ raise error
+
+ def test_single_thread(self, portals, xdg_document_portal, dbus_con):
+ run_test(3)
+
+ def test_multi_thread(self, portals, xdg_document_portal, dbus_con):
+ if xdp.run_long_tests():
+ return self.parallel(run_test, 20, 10)
+ if xdp.is_in_ci():
+ return self.parallel(run_test, 5, 3)
+ self.parallel(run_test, 10, 5)
+
+
+def run_bash(cmd):
+ proc = subprocess.Popen(
+ cmd, stdout=None, stderr=None, shell=True, universal_newlines=True
+ )
+ _ = proc.communicate()
+ return proc.returncode == 0
+
+
+if not run_bash("fusermount3 --version"):
+ pytest.skip("no fusermount3", allow_module_level=True)
+
+if not run_bash("capsh --print | grep -q 'Bounding set.*[^a-z]cap_sys_admin'"):
+ pytest.skip(
+ "No cap_sys_admin in bounding set, can't use FUSE", allow_module_level=True
+ )
+
+if not run_bash("[ -w /dev/fuse ]"):
+ pytest.skip("no write access to /dev/fuse", allow_module_level=True)
+
+if not run_bash("[ -e /etc/mtab ]"):
+ pytest.skip("no /etc/mtab", allow_module_level=True)