diff --git a/tests/__init__.py b/tests/__init__.py
index b47f9e568..e4fe56951 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -9,6 +9,7 @@
from itertools import count
from typing import Any, Dict, Optional, NamedTuple, Callable, List
+import os
import dbus
import dbus.proxies
import dbusmock
@@ -27,6 +28,10 @@
logger = logging.getLogger("tests")
+def is_in_ci() -> bool:
+ return "GITHUB_ACTIONS" in os.environ and os.environ["GITHUB_ACTIONS"] == "true"
+
+
def wait(ms: int):
"""
Waits for the specified amount of milliseconds.
diff --git a/tests/meson.build b/tests/meson.build
index 68b61dae2..bd56f4f49 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',
@@ -313,6 +304,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',
@@ -353,7 +345,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 9ea15cb61..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 "$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..22cf65c70
--- 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,73 @@ 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 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.is_in_ci():
+ parallel_tests = 10
+ parallel_iterations = 5
+ else:
+ parallel_tests = 20
+ parallel_iterations = 10
+
+ procs = []
+ for i in range(parallel_tests):
+ print(f"running proc {i}")
+ p = Process(target=run_test, 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 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)