meson
 ninja
 # Test packages:
-looseversion
 pytest
 
 
 set -e
 
-TEST_CMD="pytest -v --maxfail=1 --log-level=DEBUG --log-cli-level=DEBUG test/"
+TEST_CMD="pytest -v --maxfail=1 --log-level=INFO --log-cli-level=INFO test/"
 SAN="-Db_sanitize=address,undefined"
 
 # not default
 
 import platform
 import sys
 import os
-from looseversion import LooseVersion
+import logging
+from packaging import version
 from util import (wait_for_mount, umount, cleanup, base_cmdline,
                   safe_sleep, basename, fuse_test_marker, fuse_caps,
-                  fuse_proto, create_tmpdir)
+                  fuse_proto, create_tmpdir, parse_kernel_version)
 from os.path import join as pjoin
 import os.path
 
                     reason='not supported by running kernel')
 @pytest.mark.parametrize("writeback", (False, True))
 def test_write_cache(tmpdir, writeback, output_checker):
-    if writeback and LooseVersion(platform.release()) < '3.14':
+    if writeback and parse_kernel_version(platform.release()) < version.parse('3.14'):
         pytest.skip('Requires kernel 3.14 or newer')
     # This test hangs under Valgrind when running close(fd)
     # test_write_cache.c:test_fs(). Most likely this is because of an internal
                 mnt_dir ]
     if writeback:
         cmdline.append('-owriteback_cache')
-    elif LooseVersion(platform.release()) >= '5.16':
+    elif parse_kernel_version(platform.release()) >= version.parse('5.16'):
         # Test that close(rofd) does not block waiting for pending writes.
         # This test requires kernel commit a390ccb316be ("fuse: add FOPEN_NOFLUSH")
         # so opt-in for this test from kernel 5.16.
 @pytest.mark.parametrize("name", names)
 @pytest.mark.parametrize("notify", (True, False))
 def test_notify1(tmpdir, name, notify, output_checker):
+    logger = logging.getLogger(__name__)
     mnt_dir = str(tmpdir)
+    logger.debug(f"Mount directory: {mnt_dir}")
     create_tmpdir(mnt_dir)
     cmdline = base_cmdline + \
               [ pjoin(basename, 'example', name),
                 '-f', '--update-interval=1', mnt_dir ]
     if not notify:
         cmdline.append('--no-notify')
+    logger.debug(f"Command line: {' '.join(cmdline)}")
     mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd,
                                      stderr=output_checker.fd)
     try:
         wait_for_mount(mount_process, mnt_dir)
+        logger.debug("Mount completed")
         filename = pjoin(mnt_dir, 'current_time')
+        logger.debug(f"Target filename: {filename}")
         with open(filename, 'r') as fh:
             read1 = fh.read()
+        logger.debug(f"First read: {read1}")
+        logger.debug("Sleeping for 2 seconds...")
         safe_sleep(2)
+        logger.debug("Sleep completed")
         with open(filename, 'r') as fh:
             read2 = fh.read()
+        logger.debug(f"Second read: {read2}")
         if notify:
+            logger.debug("Expecting reads to be different")
             assert read1 != read2
         else:
+            logger.debug("Expecting reads to be the same")
             assert read1 == read2
+        logger.debug("Test completed successfully")
     except:
-        print("Failure in notify test: '" + str(cmdline) + "'")
+        logger.error(f"Failure in notify test: '{' '.join(cmdline)}'")
+        logger.exception("Exception details:")
         cleanup(mount_process, mnt_dir)
         raise
     else:
-        umount(mount_process, mnt_dir)
+        logger.debug("Unmounting...")
+        try:
+            umount(mount_process, mnt_dir)
+            logger.debug("Umount disabled")
+        except:
+            logger.error(f"Failure in unmount: '{' '.join(cmdline)}'")
+            cleanup(mount_process, mnt_dir)
+        logger.debug("Unmount completed")
 
 @pytest.mark.skipif(fuse_proto < (7,12),
                     reason='not supported by running kernel')
 @pytest.mark.parametrize("notify", (True, False))
 def test_notify_file_size(tmpdir, notify, output_checker):
+    logger = logging.getLogger(__name__)
     mnt_dir = str(tmpdir)
+    logger.debug(f"Mount directory: {mnt_dir}")
     create_tmpdir(mnt_dir)
     cmdline = base_cmdline + \
               [ pjoin(basename, 'example', 'invalidate_path'),
                 '-f', '--update-interval=1', mnt_dir ]
     if not notify:
         cmdline.append('--no-notify')
+    logger.debug(f"Command line: {' '.join(cmdline)}")
     mount_process = subprocess.Popen(cmdline, stdout=output_checker.fd,
                                      stderr=output_checker.fd)
+    logger.debug(f"Mount process PID: {mount_process.pid}")
     try:
         wait_for_mount(mount_process, mnt_dir)
         filename = pjoin(mnt_dir, 'growing')
         size = os.path.getsize(filename)
+        logger.debug(f"Initial file size: {size}")
+        logger.debug("Sleeping for 2 seconds...")
         safe_sleep(2)
+        logger.debug("Sleep completed")
         new_size = os.path.getsize(filename)
+        logger.debug(f"New file size: {new_size}")
         if notify:
             assert new_size > size
         else:
             assert new_size == size
+        logger.debug("Test completed successfully")
     except:
         cleanup(mount_process, mnt_dir)
         raise
     else:
-        umount(mount_process, mnt_dir)
+        try:
+            umount(mount_process, mnt_dir)
+        except:
+            logger.error(f"Failure in unmount: '{' '.join(cmdline)}'")
+            cleanup(mount_process, mnt_dir)
+        logger.debug("Unmount completed")
 
 import errno
 import sys
 import platform
-from looseversion import LooseVersion
+import re
+from packaging import version
 from tempfile import NamedTemporaryFile
 from contextlib import contextmanager
 from util import (wait_for_mount, umount, cleanup, base_cmdline,
                   safe_sleep, basename, fuse_test_marker, test_printcap,
-                  fuse_proto, fuse_caps, powerset)
+                  fuse_proto, fuse_caps, powerset, parse_kernel_version)
 from os.path import join as pjoin
+import logging
 
 pytestmark = fuse_test_marker()
 
 @pytest.mark.parametrize("options", powerset(options))
 @pytest.mark.parametrize("name", ('hello', 'hello_ll'))
 def test_hello(tmpdir, name, options, cmdline_builder, output_checker):
+    logger = logging.getLogger(__name__)
     mnt_dir = str(tmpdir)
+    logger.debug(f"Mount directory: {mnt_dir}")
+    cmdline = cmdline_builder(mnt_dir, name, options)
+    logger.debug(f"Command line: {' '.join(cmdline)}")
     mount_process = subprocess.Popen(
-        cmdline_builder(mnt_dir, name, options),
+        cmdline,
         stdout=output_checker.fd, stderr=output_checker.fd)
+    logger.debug(f"Mount process PID: {mount_process.pid}")
     try:
+        logger.debug("Waiting for mount...")
         wait_for_mount(mount_process, mnt_dir)
+        logger.debug("Mount completed")
         assert os.listdir(mnt_dir) == [ 'hello' ]
+        logger.debug("Verified 'hello' file exists in mount directory")
         filename = pjoin(mnt_dir, 'hello')
         with open(filename, 'r') as fh:
             assert fh.read() == 'Hello World!\n'
+        logger.debug("Verified contents of 'hello' file")
         with pytest.raises(IOError) as exc_info:
             open(filename, 'r+')
         assert exc_info.value.errno == errno.EACCES
+        logger.debug("Verified EACCES error when trying to open file for writing")
         with pytest.raises(IOError) as exc_info:
             open(filename + 'does-not-exist', 'r+')
         assert exc_info.value.errno == errno.ENOENT
+        logger.debug("Verified ENOENT error for non-existent file")
         if name == 'hello_ll':
+            logger.debug("Testing xattr for hello_ll")
             tst_xattr(mnt_dir)
             path = os.path.join(mnt_dir, 'hello')
             tst_xattr(path)
     except:
+        logger.error("Exception occurred during test", exc_info=True)
         cleanup(mount_process, mnt_dir)
         raise
     else:
+        logger.debug("Unmounting...")
         umount(mount_process, mnt_dir)
+        logger.debug("Test completed successfully")
 
 @pytest.mark.parametrize("writeback", (False, True))
 @pytest.mark.parametrize("name", ('passthrough', 'passthrough_plus',
             # unlinked testfiles check fails without kernel fix
             # "fuse: fix illegal access to inode with reused nodeid"
             # so opt-in for this test from kernel 5.14
-            if LooseVersion(platform.release()) >= '5.14':
+            if parse_kernel_version(platform.release()) >= version.parse('5.14'):
                 syscall_test_cmd.append('-u')
             subprocess.check_call(syscall_test_cmd)
     except:
 
 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,
         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 ]
 
-    subprocess.check_call(cmdline)
-    assert not os.path.ismount(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...
         if code is not None:
             if code == 0:
                 return
-            pytest.fail('file system process terminated with code %s' % (code,))
+            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')