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)