Merge pull request #2094 from pbiering/tox-test-filesystems

Tox test filesystems
This commit is contained in:
Peter Bieringer
2026-04-19 12:25:39 +02:00
committed by GitHub
8 changed files with 274 additions and 25 deletions

View File

@@ -30,7 +30,7 @@ on:
jobs: jobs:
test-ubuntu-python-newest: test-ubuntu-python-newest:
name: Test Ubuntu:latest Python:newest name: Test Python:newest Ubuntu:latest
needs: lint needs: lint
strategy: strategy:
matrix: matrix:
@@ -48,7 +48,7 @@ jobs:
run: tox -c pyproject.toml -e py run: tox -c pyproject.toml -e py
test-ubuntu-python-newest-with-passlib: test-ubuntu-python-newest-with-passlib:
name: Test Ubuntu:latest Python:newest passlib name: Test passlib Python:newest Ubuntu:latest
needs: [lint, test-ubuntu-python-newest] needs: [lint, test-ubuntu-python-newest]
strategy: strategy:
matrix: matrix:
@@ -64,12 +64,48 @@ jobs:
run: pip install tox run: pip install tox
- name: Switch back to passlib - name: Switch back to passlib
run: sed -i 's|libpass[^"]*|passlib|' pyproject.toml run: sed -i 's|libpass[^"]*|passlib|' pyproject.toml
- name: Test with newest Python on latest Ubuntu - name: Test with newest Python on latest Ubuntu using passlib
run: tox -c pyproject.toml -e py run: tox -c pyproject.toml -e py
test-ubuntu-python-newest-with-vfat:
name: Test VFAT Python:newest Ubuntu:latest
needs: [lint, test-ubuntu-python-newest]
strategy:
matrix:
os: [ubuntu-latest]
python-version: ['3.14']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install Test dependencies
run: pip install tox
- name: Test with newest Python on latest Ubuntu using VFAT
run: tox -c pyproject.toml -e py_filesystem_vfat
test-ubuntu-python-newest-with-ntfs:
name: Test NTFS Python:newest Ubuntu:latest
needs: [lint, test-ubuntu-python-newest]
strategy:
matrix:
os: [ubuntu-latest]
python-version: ['3.14']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install Test dependencies
run: pip install tox
- name: Test with newest Python on latest Ubuntu using NTFS
run: tox -c pyproject.toml -e py_filesystem_ntfs
test-python-32bit: test-python-32bit:
name: Test Ubuntu:latest Python:32-bit name: Test 32-bit Python:3.11 Ubuntu:latest
needs: [lint, test-ubuntu-python-newest, integ-test] needs: [lint, test-ubuntu-python-newest, integ-test, test-ubuntu-python-newest-with-passlib]
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
@@ -102,13 +138,13 @@ jobs:
python3 -m venv venv python3 -m venv venv
. venv/bin/activate . venv/bin/activate
pip install --upgrade pip pip install --upgrade pip
- name: Test 32-bit - name: Test with 32-bit Python on latest Ubuntu
run: | run: |
. venv/bin/activate . venv/bin/activate
tox -c /__w/Radicale/Radicale/pyproject.toml -e py tox -c /__w/Radicale/Radicale/pyproject.toml -e py
test-ubuntu-python-oldest: test-ubuntu-python-oldest:
name: Test Ubuntu:latest Python:oldest name: Test Python:oldest Ubuntu:latest
needs: [lint, test-ubuntu-python-newest, test-python-32bit] needs: [lint, test-ubuntu-python-newest, test-python-32bit]
strategy: strategy:
matrix: matrix:
@@ -127,7 +163,7 @@ jobs:
test-otheros-python-newest: test-otheros-python-newest:
name: Test MacOS/Windows:latest Python:newest name: Test MacOS/Windows:latest Python:newest
needs: [lint, test-ubuntu-python-newest, test-python-32bit] needs: [lint, test-ubuntu-python-newest, test-ubuntu-python-newest-with-ntfs, test-ubuntu-python-newest-with-vfat]
strategy: strategy:
matrix: matrix:
os: [macos-latest, windows-latest] os: [macos-latest, windows-latest]
@@ -229,7 +265,7 @@ jobs:
js-test: js-test:
name: JS Type Check name: JS Type Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: test-ubuntu-python-newest needs: [test-ubuntu-python-newest]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-node@v5 - uses: actions/setup-node@v5

View File

@@ -5,6 +5,9 @@
* Improve: `path_to_filesystem()` by pre-detection of collision-free file system * Improve: `path_to_filesystem()` by pre-detection of collision-free file system
* Adjustment: MKCOL/MKCALENDAR return now CONFLICT instead of BADREQUEST of file name collision * Adjustment: MKCOL/MKCALENDAR return now CONFLICT instead of BADREQUEST of file name collision
* Improve: [auth] catch bcrypt>=5.0.0 enforced max password length early and support legacy "passlib" as well as "libpass" (rework 3.6.0, "packaging" not needed anymore) * Improve: [auth] catch bcrypt>=5.0.0 enforced max password length early and support legacy "passlib" as well as "libpass" (rework 3.6.0, "packaging" not needed anymore)
* Improve: application will stop on startup if TEMP is provided but not existing or not writable
* Extension: tox with new optional test cases to test with LinuxOS vfat, hfsplus, ntfs filesystems
* Adjust: respond with 500 in case principal collection cannot be created (e.g. filesystem issues)
## 3.7.1 ## 3.7.1
* Fix: share address book collection as birthday calendar not working on non-DEBUG level * Fix: share address book collection as birthday calendar not working on non-DEBUG level

View File

@@ -87,6 +87,88 @@ deps = [
] ]
commands = [["pytest", "-r", "s", "--log-level", "5", "."]] commands = [["pytest", "-r", "s", "--log-level", "5", "."]]
[tool.tox.env.py_filesystem_vfat]
allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
extras = ["test"]
deps = [
"pytest"
]
commands_pre = [
# create 64 MByte disk image
["dd", "if=/dev/zero", "of=/tmp/vfat.img", "bs=1M", "count=64"],
# create file system
["mkfs.vfat", "/tmp/vfat.img", "-n", "VFAT"],
# unconditionally create mount point
["mkdir", "-p", "/tmp/vfat"],
# mount image
["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/vfat.img", "/tmp/vfat"],
]
setenv = { TEMP = "/tmp/vfat" }
commands = [["pytest", "-r", "s", "."]]
commands_post = [
# umount image
["sudo", "/usr/bin/umount", "-d", "/tmp/vfat"],
# remove mount point
["rmdir", "/tmp/vfat"],
# remove image
["rm", "/tmp/vfat.img"]
]
[tool.tox.env.py_filesystem_hfsplus]
# Fedora
allowlist_externals = [ "sudo", "dd", "mkfs.hfsplus", "mkdir", "rm", "rmdir", "chmod" ]
extras = ["test"]
deps = [
"pytest"
]
commands_pre = [
# create 64 MByte disk image
["dd", "if=/dev/zero", "of=/tmp/hfsp.img", "bs=1M", "count=64"],
# create file system
["mkfs.hfsplus", "/tmp/hfsp.img", "-v", "HFSP"],
# unconditionally create mount point
["mkdir", "-p", "/tmp/hfsp"],
# mount image
["sudo", "/usr/bin/mount", "-o", "loop,umask=000", "/tmp/hfsp.img", "/tmp/hfsp", "-t", "hfsplus"],
]
setenv = { TEMP = "/tmp/hfsp" }
commands = [["pytest", "-r", "s", "."]]
commands_post = [
# umount image
["sudo", "/usr/bin/umount", "-d", "/tmp/hfsp"],
# remove mount point
["rmdir", "/tmp/hfsp"],
# remove image
["rm", "/tmp/hfsp.img"]
]
[tool.tox.env.py_filesystem_ntfs]
allowlist_externals = [ "sudo", "dd", "mkfs.ntfs", "mkdir", "rm", "rmdir", "chmod" ]
extras = ["test"]
deps = [
"pytest"
]
commands_pre = [
# create 64 MByte disk image
["dd", "if=/dev/zero", "of=/tmp/ntfs.img", "bs=1M", "count=64"],
# create file system
["mkfs.ntfs", "/tmp/ntfs.img", "-L", "NTFS", "-F"],
# unconditionally create mount point
["mkdir", "-p", "/tmp/ntfs"],
# mount image
["sudo", "/usr/bin/mount", "-o", "loop,umask=0000", "/tmp/ntfs.img", "/tmp/ntfs"],
]
setenv = { TEMP = "/tmp/ntfs" }
commands = [["pytest", "-r", "s", "."]]
commands_post = [
# umount image
["sudo", "/usr/bin/umount", "-d", "/tmp/ntfs"],
# remove mount point
["rmdir", "/tmp/ntfs"],
# remove image
["rm", "/tmp/ntfs.img"]
]
[tool.tox.env.flake8] [tool.tox.env.flake8]
deps = ["flake8==7.1.0"] deps = ["flake8==7.1.0"]
commands = [["flake8", "."]] commands = [["flake8", "."]]

View File

@@ -100,6 +100,11 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
""" """
super().__init__(configuration) super().__init__(configuration)
if 'TEMP' in os.environ:
if not os.path.isdir(os.environ['TEMP']):
raise RuntimeError("TEMP found in environment, but directory is not existing: %r" % os.environ['TEMP'])
if not os.access(os.environ['TEMP'], os.W_OK):
raise RuntimeError("TEMP found in environment, but not writable: %r" % os.environ['TEMP'])
self._mask_passwords = configuration.get("logging", "mask_passwords") self._mask_passwords = configuration.get("logging", "mask_passwords")
self._delay_on_error = configuration.get("server", "delay_on_error") self._delay_on_error = configuration.get("server", "delay_on_error")
logger.info("delay_on_error set to: %.3f seconds", self._delay_on_error) logger.info("delay_on_error set to: %.3f seconds", self._delay_on_error)
@@ -535,9 +540,13 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
except ValueError as e: except ValueError as e:
logger.warning("Failed to create predefined collection %r: %s", name_coll, e) logger.warning("Failed to create predefined collection %r: %s", name_coll, e)
except ValueError as e: except ValueError as e:
logger.warning("Failed to create principal " logger.error("Failed to create principal "
"collection %r: %s", user, e) "collection for user %r: ValueError %s", user, e)
user = "" return response(*httputils.INTERNAL_SERVER_ERROR)
except OSError as e:
logger.error("Failed to create principal "
"collection for user %r: OSerror %s", user, e)
return response(*httputils.INTERNAL_SERVER_ERROR)
else: else:
logger.warning("Access to principal path %r denied by " logger.warning("Access to principal path %r denied by "
"rights backend", principal_path) "rights backend", principal_path)

View File

@@ -407,7 +407,7 @@ def path_supports_symlink(path):
def path_is_collision_free_case_sensitive(path): def path_is_collision_free_case_sensitive(path):
# Test: case sensitive """Check whether path supports case sensitive entries."""
if not os.path.isdir(path): if not os.path.isdir(path):
raise ValueError("%r is not a path" % (path)) raise ValueError("%r is not a path" % (path))
base_dir = tempfile.mkdtemp(dir=path) base_dir = tempfile.mkdtemp(dir=path)
@@ -426,7 +426,7 @@ def path_is_collision_free_case_sensitive(path):
# cleanup # cleanup
os.rmdir(test_dir_uc) os.rmdir(test_dir_uc)
os.rmdir(base_dir) os.rmdir(base_dir)
logger.debug("path_is_collision_free (case-sensitive): path=%r result=%s", path, result) logger.trace("path_is_collision_free (case-sensitive): path=%r result=%s", path, result)
return result return result
@@ -450,5 +450,78 @@ def path_is_collision_free_no_short_filename(path):
# cleanup # cleanup
os.rmdir(test_dir_long) os.rmdir(test_dir_long)
os.rmdir(base_dir) os.rmdir(base_dir)
logger.debug("path_is_collision_free (no short-filename): path=%r result=%s", path, result) logger.trace("path_is_collision_free (no short-filename): path=%r result=%s", path, result)
return result
def path_supports_unicode(path):
"""Check whether path supports unicode."""
if not os.path.isdir(path):
raise ValueError("%r is not a path" % (path))
base_dir = tempfile.mkdtemp(dir=path)
part = "TEST😀"
test_dir = os.path.join(base_dir, part)
result = True
try:
os.mkdir(test_dir)
except OSError:
result = False
else:
with os.scandir(base_dir) as entries:
if part not in (e.name for e in entries):
result = False
# cleanup
os.rmdir(test_dir)
# cleanup
os.rmdir(base_dir)
logger.trace("path_supports_unicode: path=%r result=%s", path, result)
return result
def path_supports_trailing_whitespace(path):
"""Check whether path supports trailing whitespace."""
if not os.path.isdir(path):
raise ValueError("%r is not a path" % (path))
base_dir = tempfile.mkdtemp(dir=path)
part = "TEST "
test_dir = os.path.join(base_dir, part)
result = True
try:
os.mkdir(test_dir)
except OSError:
result = False
else:
with os.scandir(base_dir) as entries:
if part not in (e.name for e in entries):
result = False
# cleanup
os.rmdir(test_dir)
# cleanup
os.rmdir(base_dir)
logger.trace("path_supports_trailing_whitespace: path=%r result=%s", path, result)
return result
def path_supports_problematic_chars(path):
"""Check whether path supports problematic chars."""
if not os.path.isdir(path):
raise ValueError("%r is not a path" % (path))
base_dir = tempfile.mkdtemp(dir=path)
result = True
for char in ['*', '?']:
part = "TES" + char + "T"
test_dir = os.path.join(base_dir, part)
try:
os.mkdir(test_dir)
except OSError:
result = False
else:
with os.scandir(base_dir) as entries:
if part not in (e.name for e in entries):
result = False
# cleanup
os.rmdir(test_dir)
# cleanup
os.rmdir(base_dir)
logger.trace("path_supports_problematic chars: path=%r result=%s", path, result)
return result return result

View File

@@ -110,7 +110,7 @@ class Storage(
except Exception as e: except Exception as e:
logger.warning("Storage item mtime resolution test not possible, cannot set utime on file: %r (%s)", path, e) logger.warning("Storage item mtime resolution test not possible, cannot set utime on file: %r (%s)", path, e)
os.remove(path) os.remove(path)
raise raise ValueError # do not raise a hard PermissionError
logger.debug("Storage item mtime resoultion test set: %d ns" % MTIME_NS_TEST) logger.debug("Storage item mtime resoultion test set: %d ns" % MTIME_NS_TEST)
mtime_ns = os.stat(path).st_mtime_ns - mtime_ns mtime_ns = os.stat(path).st_mtime_ns - mtime_ns
logger.debug("Storage item mtime resoultion test get: %d ns" % mtime_ns) logger.debug("Storage item mtime resoultion test get: %d ns" % mtime_ns)
@@ -180,6 +180,9 @@ class Storage(
self._filesystem_root_folder_is_collision_free, self._filesystem_root_folder_is_collision_free,
filesystem_root_folder_is_collision_free_case_sensitive, filesystem_root_folder_is_collision_free_case_sensitive,
filesystem_root_folder_is_collision_free_no_short_filename) filesystem_root_folder_is_collision_free_no_short_filename)
logger.info("Storage location subfolder suppports unicode: %s", pathutils.path_supports_unicode(self._get_collection_root_folder()))
logger.info("Storage location subfolder suppports trailing whitespace: %s", pathutils.path_supports_trailing_whitespace(self._get_collection_root_folder()))
logger.info("Storage location subfolder suppports problematic chars: %s", pathutils.path_supports_problematic_chars(self._get_collection_root_folder()))
logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item) logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item)
logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history) logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history)
logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken) logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken)

View File

@@ -31,7 +31,7 @@ from typing import Iterable, Tuple, Union
import pytest import pytest
from radicale import xmlutils from radicale import pathutils, xmlutils
from radicale.auth import htpasswd from radicale.auth import htpasswd
from radicale.tests import BaseTest from radicale.tests import BaseTest
@@ -64,7 +64,7 @@ class TestBaseAuthRequests(BaseTest):
def _test_htpasswd(self, htpasswd_encryption: str, htpasswd_content: str, def _test_htpasswd(self, htpasswd_encryption: str, htpasswd_content: str,
test_matrix: Union[str, Iterable[Tuple[str, str, bool]]] test_matrix: Union[str, Iterable[Tuple[str, str, bool]]]
= "ascii", delay: float = 0) -> None: = "ascii", delay: float = 0, check: int = 207) -> None:
"""Test htpasswd authentication with user "tmp" and password "bepo" for """Test htpasswd authentication with user "tmp" and password "bepo" for
``test_matrix`` "ascii" or user "😀" and password "🔑" for ``test_matrix`` "ascii" or user "😀" and password "🔑" for
``test_matrix`` "unicode".""" ``test_matrix`` "unicode"."""
@@ -88,7 +88,7 @@ class TestBaseAuthRequests(BaseTest):
elif isinstance(test_matrix, str): elif isinstance(test_matrix, str):
raise ValueError("Unknown test matrix %r" % test_matrix) raise ValueError("Unknown test matrix %r" % test_matrix)
for user, password, valid in test_matrix: for user, password, valid in test_matrix:
self.propfind("/", check=207 if valid else 401, self.propfind("/", check=check if valid else 401,
login="%s:%s" % (user, password)) login="%s:%s" % (user, password))
def test_htpasswd_plain(self) -> None: def test_htpasswd_plain(self) -> None:
@@ -102,7 +102,11 @@ class TestBaseAuthRequests(BaseTest):
("tmp", "be:po", True), ("tmp", "bepo", False))) ("tmp", "be:po", True), ("tmp", "bepo", False)))
def test_htpasswd_plain_unicode(self) -> None: def test_htpasswd_plain_unicode(self) -> None:
self._test_htpasswd("plain", "😀:🔑", "unicode") if not pathutils.path_supports_unicode(self.colpath):
check = 500
else:
check = 207
self._test_htpasswd("plain", "😀:🔑", "unicode", check=check)
def test_htpasswd_md5(self) -> None: def test_htpasswd_md5(self) -> None:
self._test_htpasswd("md5", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") self._test_htpasswd("md5", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/")
@@ -111,8 +115,12 @@ class TestBaseAuthRequests(BaseTest):
self._test_htpasswd("autodetect", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/") self._test_htpasswd("autodetect", "tmp:$apr1$BI7VKCZh$GKW4vq2hqDINMr8uv7lDY/")
def test_htpasswd_md5_unicode(self): def test_htpasswd_md5_unicode(self):
if not pathutils.path_supports_unicode(self.colpath):
check = 500
else:
check = 207
self._test_htpasswd( self._test_htpasswd(
"md5", "😀:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode") "md5", "😀:$apr1$w4ev89r1$29xO8EvJmS2HEAadQ5qy11", "unicode", check=check)
def test_htpasswd_sha256(self) -> None: def test_htpasswd_sha256(self) -> None:
self._test_htpasswd("sha256", "tmp:$5$i4Ni4TQq6L5FKss5$ilpTjkmnxkwZeV35GB9cYSsDXTALBn6KtWRJAzNlCL/") self._test_htpasswd("sha256", "tmp:$5$i4Ni4TQq6L5FKss5$ilpTjkmnxkwZeV35GB9cYSsDXTALBn6KtWRJAzNlCL/")
@@ -166,7 +174,11 @@ class TestBaseAuthRequests(BaseTest):
@pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed")
def test_htpasswd_bcrypt_unicode(self) -> None: def test_htpasswd_bcrypt_unicode(self) -> None:
self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode") if not pathutils.path_supports_unicode(self.colpath):
check = 500
else:
check = 207
self._test_htpasswd("bcrypt", "😀:$2y$10$Oyz5aHV4MD9eQJbk6GPemOs4T6edK6U9Sqlzr.W1mMVCS8wJUftnW", "unicode", check=check)
@pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed") @pytest.mark.skipif(has_bcrypt == 0, reason="No bcrypt module installed")
def test_htpasswd_bcrypt_long(self) -> None: def test_htpasswd_bcrypt_long(self) -> None:
@@ -280,12 +292,23 @@ class TestBaseAuthRequests(BaseTest):
else: else:
raise raise
@pytest.mark.skipif(sys.platform == "win32", reason="leading and trailing "
"whitespaces not allowed in file names")
def test_htpasswd_whitespace_user(self) -> None: def test_htpasswd_whitespace_user(self) -> None:
for user in (" tmp", "tmp ", " tmp "): for user in (" tmp", "tmp ", " tmp "):
if not pathutils.path_supports_trailing_whitespace(self.colpath) and user.endswith(' '):
check = 500
else:
check = 207
self._test_htpasswd("plain", "%s:bepo" % user, ( self._test_htpasswd("plain", "%s:bepo" % user, (
(user, "bepo", True), ("tmp", "bepo", False))) (user, "bepo", True), ("tmp", "bepo", False)), check=check)
def test_htpasswd_problem_user(self) -> None:
for user in ("tm*p", "tm?p"):
if not pathutils.path_supports_problematic_chars(self.colpath):
check = 500
else:
check = 207
self._test_htpasswd("plain", "%s:bepo" % user, (
(user, "bepo", True), ("tmp", "bepo", False)), check=check)
def test_htpasswd_whitespace_password(self) -> None: def test_htpasswd_whitespace_password(self) -> None:
for password in (" bepo", "bepo ", " bepo "): for password in (" bepo", "bepo ", " bepo "):

View File

@@ -190,6 +190,26 @@ class TestMultiFileSystem(BaseTest):
assert answer is not None assert answer is not None
assert "\r\nUID:%s\r\n" % uid in answer assert "\r\nUID:%s\r\n" % uid in answer
@pytest.mark.skipif(not pathutils.path_is_collision_free_case_sensitive(tempfile.mkdtemp()), reason="TEMP is not case sensitive")
def test_collection_storage_dummy_case_sensitivity(self) -> None:
"""Test collection storage case sensitivity."""
@pytest.mark.skipif(not pathutils.path_is_collision_free_no_short_filename(tempfile.mkdtemp()), reason="TEMP has short filename")
def test_collection_storage_dummy_no_short_filename(self) -> None:
"""Test collection storage no short filename."""
@pytest.mark.skipif(not pathutils.path_supports_unicode(tempfile.mkdtemp()), reason="TEMP is not supporting unicode")
def test_collection_storage_dummy_no_support_of_unicode(self) -> None:
"""Test collection storage no support of unicode."""
@pytest.mark.skipif(not pathutils.path_supports_trailing_whitespace(tempfile.mkdtemp()), reason="TEMP is not supporting trailing whitespace")
def test_collection_storage_dummy_no_support_of_trailing_whitespace(self) -> None:
"""Test collection storage no support of trailing space."""
@pytest.mark.skipif(not pathutils.path_supports_problematic_chars(tempfile.mkdtemp()), reason="TEMP is not supporting problematic chars")
def test_collection_storage_dummy_no_support_of_problematic_chars(self) -> None:
"""Test collection storage no support of problematic chars."""
@pytest.mark.skipif(not pathutils.path_supports_symlink(tempfile.mkdtemp()), reason="TEMP is not supporting symlink") @pytest.mark.skipif(not pathutils.path_supports_symlink(tempfile.mkdtemp()), reason="TEMP is not supporting symlink")
def test_collection_sharing_by_softlink(self) -> None: def test_collection_sharing_by_softlink(self) -> None:
"""Test collection sharing by softlink.""" """Test collection sharing by softlink."""