Files
libfuse/test/util.py
Bernd Schubert efa6c4c8be Fix BaseException catch in test files
Replace bare except clauses with 'except Exception' to avoid
catching system-exiting exceptions like KeyboardInterrupt and
SystemExit, which should not be suppressed.

Fixes: GitHub CodeQL Alerts #15, #16, #17
Rule: py/catch-base-exception
Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-09 16:47:20 +02:00

258 lines
8.7 KiB
Python

#!/usr/bin/env python3
import subprocess
import pytest
import os
import stat
import time
from os.path import join as pjoin
import sys
import re
import itertools
from packaging import version
import logging
basename = pjoin(os.path.dirname(__file__), '..')
def parse_kernel_version(release):
# Extract the first three numbers from the kernel version string
match = re.match(r'^(\d+\.\d+\.\d+)', release)
if match:
return version.parse(match.group(1))
return version.parse('0')
def get_printcap():
cmdline = base_cmdline + [ pjoin(basename, 'example', 'printcap') ]
proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE,
universal_newlines=True)
(stdout, _) = proc.communicate(30)
assert proc.returncode == 0
proto = None
caps = set()
for line in stdout.split('\n'):
if line.startswith('\t'):
caps.add(line.strip())
continue
hit = re.match(r'Protocol version: (\d+)\.(\d+)$', line)
if hit:
proto = (int(hit.group(1)), int(hit.group(2)))
return (proto, caps)
def test_printcap():
get_printcap()
return None
def wait_for_mount(mount_process, mnt_dir,
test_fn=os.path.ismount):
elapsed = 0
while elapsed < 30:
if test_fn(mnt_dir):
return True
if mount_process.poll() is not None:
if test_fn(mnt_dir):
return True
pytest.fail('file system process terminated prematurely')
time.sleep(0.1)
elapsed += 0.1
pytest.fail("mountpoint failed to come up")
def cleanup(mount_process, mnt_dir):
# Don't bother trying Valgrind if things already went wrong
if 'bsd' in sys.platform or 'dragonfly' in sys.platform:
cmd = [ 'umount', '-f', mnt_dir ]
else:
cmd = [pjoin(basename, 'util', 'fusermount3'),
'-z', '-u', mnt_dir]
subprocess.call(cmd, stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
mount_process.terminate()
try:
mount_process.wait(1)
except subprocess.TimeoutExpired:
mount_process.kill()
def umount(mount_process, mnt_dir):
logger = logging.getLogger(__name__)
logger.debug(f"Unmounting {mnt_dir}")
if 'bsd' in sys.platform or 'dragonfly' in sys.platform:
cmdline = [ 'umount', mnt_dir ]
logger.debug("Using BSD-style umount command")
else:
logger.debug("Using fusermount3 for unmounting")
# fusermount3 will be setuid root, so we can only trace it with
# valgrind if we're root
if os.getuid() == 0:
cmdline = base_cmdline
logger.debug("Running as root, using valgrind if configured")
else:
cmdline = []
logger.debug("Not running as root, skipping valgrind for fusermount3")
cmdline = cmdline + [ pjoin(basename, 'util', 'fusermount3'),
'-z', '-u', mnt_dir ]
logger.debug(f"Unmount command: {' '.join(cmdline)}")
try:
result = subprocess.run(cmdline, capture_output=True, text=True, check=True)
if result.stdout:
logger.debug(f"Unmount command stdout: {result.stdout}")
if result.stderr:
logger.debug(f"Unmount command stderr: {result.stderr}")
except subprocess.CalledProcessError as e:
logger.error(f"Unmount command failed with return code {e.returncode}\nStdout: {e.stdout}\nStderr: {e.stderr}")
raise
if not os.path.ismount(mnt_dir):
logger.debug(f"{mnt_dir} is no longer a mount point")
else:
logger.warning(f"{mnt_dir} is still a mount point after unmount command")
# Give mount process a little while to terminate. Popen.wait(timeout)
# was only added in 3.3...
elapsed = 0
while elapsed < 30:
code = mount_process.poll()
if code is not None:
if code == 0:
return
logger.error(f"File system process terminated with code {code}")
pytest.fail(f'file system process terminated with code {code}')
time.sleep(0.1)
elapsed += 0.1
logger.error("Mount process did not terminate within 30 seconds")
pytest.fail('mount process did not terminate')
def parse_mountinfo(mnt_dir):
'''Return the /proc/self/mountinfo entry for *mnt_dir*, or None.
Parses the line for the mountpoint and returns a dict with keys:
'mountpoint' - the mount point path (str)
'fstype' - filesystem type as the kernel sees it,
e.g. 'fuse' or 'fuse.<subtype>' (str)
'source' - mount source field, e.g. 'hello' or
'<subtype>#<fsname>' fallback form (str)
'mount_options' - per-mount options/attrs (set of str), e.g.
{'rw','nosuid','nodev','noatime','relatime'}
'super_options' - superblock options from the filesystem (set of
str), e.g. {'rw','user_id=1000','group_id=1000',
'default_permissions','allow_other','max_read=...'}
These fields are exactly what /proc/self/mountinfo exposes (see
`man 5 proc.5_mountinfo`); they capture the post-mount state that
differs between the legacy mount(2) path and the new fsopen/fsconfig/
fsmount path. Asserting on this dict catches bugs where one path
drops a parameter the other passes (e.g. `subtype` showing up in
`fstype`, `user=` ending up only in /run/mount/utab, ...).
'''
target = os.path.realpath(mnt_dir)
with open('/proc/self/mountinfo') as fh:
for line in fh:
parts = line.rstrip('\n').split(' ')
try:
sep = parts.index('-')
except ValueError:
continue
if len(parts) < sep + 4 or sep < 6:
continue
mountpoint = parts[4].replace('\\040', ' ')
if mountpoint != target:
continue
return {
'mountpoint': mountpoint,
'fstype': parts[sep + 1],
'source': parts[sep + 2].replace('\\040', ' '),
'mount_options': set(parts[5].split(',')),
'super_options': set(parts[sep + 3].split(',')),
}
return None
def safe_sleep(secs):
'''Like time.sleep(), but sleep for at least *secs*
`time.sleep` may sleep less than the given period if a signal is
received. This function ensures that we sleep for at least the
desired time.
'''
now = time.time()
end = now + secs
while now < end:
time.sleep(end - now)
now = time.time()
def fuse_test_marker():
'''Return a pytest.marker that indicates FUSE availability
If system/user/environment does not support FUSE, return
a `pytest.mark.skip` object with more details. If FUSE is
supported, return `pytest.mark.uses_fuse()`.
'''
skip = lambda x: pytest.mark.skip(reason=x)
if 'bsd' in sys.platform or 'dragonfly' in sys.platform:
return pytest.mark.uses_fuse()
with subprocess.Popen(['which', 'fusermount3'], stdout=subprocess.PIPE,
universal_newlines=True) as which:
fusermount_path = which.communicate()[0].strip()
if not fusermount_path or which.returncode != 0:
return skip("Can't find fusermount executable")
if not os.path.exists('/dev/fuse'):
return skip("FUSE kernel module does not seem to be loaded")
if os.getuid() == 0:
return pytest.mark.uses_fuse()
mode = os.stat(fusermount_path).st_mode
if mode & stat.S_ISUID == 0:
return skip('fusermount executable not setuid, and we are not root.')
try:
fd = os.open('/dev/fuse', os.O_RDWR)
except OSError as exc:
return skip('Unable to open /dev/fuse: %s' % exc.strerror)
else:
os.close(fd)
return pytest.mark.uses_fuse()
def powerset(iterable):
s = list(iterable)
return itertools.chain.from_iterable(
itertools.combinations(s, r) for r in range(len(s)+1))
def create_tmpdir(mnt_dir):
if not os.path.exists(mnt_dir):
print("makedirs: '" + mnt_dir + "'")
os.makedirs(mnt_dir)
else:
print("mnt_dir exists: '" + mnt_dir + "'")
# Use valgrind if requested
if os.environ.get('TEST_WITH_VALGRIND', 'no').lower().strip() \
not in ('no', 'false', '0'):
base_cmdline = [ 'valgrind', '-q', '--' ]
else:
base_cmdline = []
# Try to use local fusermount3
os.environ['PATH'] = '%s:%s' % (pjoin(basename, 'util'), os.environ['PATH'])
# Put example binaries on PATH
os.environ['PATH'] = '%s:%s' % (pjoin(basename, 'example'), os.environ['PATH'])
try:
(fuse_proto, fuse_caps) = get_printcap()
except Exception:
# Rely on test to raise error
fuse_proto = (0,0)
fuse_caps = set()