From dba71c9b382e6004363ed34a49dd45d09e582492 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Wed, 4 Dec 2024 18:13:35 +0100 Subject: [PATCH] tests/document-fuse: Move to the pytest harness The pytest harness already knows how to set up all the portal bits and pieces so we can drop the shell script and the dbus service activation configuration. --- tests/.mypy.ini | 2 +- tests/README.md | 3 + tests/__init__.py | 4 + tests/meson.build | 11 +- tests/test-document-fuse.sh | 103 ------------ ...document-fuse.py => test_document_fuse.py} | 148 ++++++++++++------ 6 files changed, 108 insertions(+), 163 deletions(-) delete mode 100755 tests/test-document-fuse.sh rename tests/{test-document-fuse.py => test_document_fuse.py} (93%) mode change 100755 => 100644 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)