diff --git a/CHANGELOG.md b/CHANGELOG.md index b05308ab..3ef6e842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## Unreleased + +- Added search for Simvue configuration within the parent file hierarchy of the current directory. + ## [v2.3.0](https://github.com/simvue-io/client/releases/tag/v2.3.0) - 2025-12-11 - Refactored sender functionality introducing new `Sender` class. diff --git a/simvue/utilities.py b/simvue/utilities.py index e890698e..faa567ec 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -33,6 +33,7 @@ def find_first_instance_of_file( Parameters ---------- + file_names: list[str] | str candidate names of file to locate check_user_space: bool, optional check the users home area if current working directory is not @@ -47,9 +48,16 @@ def find_first_instance_of_file( file_names = [file_names] for file_name in file_names: - _user_file = pathlib.Path.cwd().joinpath(file_name) - if _user_file.exists(): - return _user_file + _current_path = pathlib.Path.cwd() + + while os.access(_current_path, os.R_OK): + _user_file = _current_path.joinpath(file_name) + if _user_file.exists(): + return _user_file + _parent_path = _current_path.parent + if _parent_path == _current_path: # parent of root dir is root dir + break + _current_path = _parent_path # If the user is running on different mounted volume or outside # of their user space then the above will not return the file diff --git a/tests/functional/test_utilities.py b/tests/functional/test_utilities.py index 8f953be4..1d8e0abd 100644 --- a/tests/functional/test_utilities.py +++ b/tests/functional/test_utilities.py @@ -1,6 +1,10 @@ import pytest import tempfile import os.path +import pathlib +import stat + +from pytest_mock import MockerFixture import simvue.utilities as sv_util @@ -19,3 +23,63 @@ def test_calculate_hash(is_file: bool, hash: str) -> None: assert sv_util.calculate_sha256(filename=out_file, is_file=is_file) == hash else: assert sv_util.calculate_sha256(filename="temp.txt", is_file=is_file) == hash + +@pytest.mark.config +@pytest.mark.parametrize( + "user_area", (True, False), + ids=("permitted_dir", "out_of_user_area") +) +def test_find_first_file_search(user_area: bool, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None: + # Deactivate the server checks for this test + monkeypatch.setenv("SIMVUE_NO_SERVER_CHECK", "True") + monkeypatch.delenv("SIMVUE_TOKEN", False) + monkeypatch.delenv("SIMVUE_URL", False) + + with tempfile.TemporaryDirectory() as temp_d: + _path = pathlib.Path(temp_d) + _path_sub = _path.joinpath("level_0") + _path_sub.mkdir() + + for i in range(1, 5): + _path_sub = _path_sub.joinpath(f"level_{i}") + _path_sub.mkdir() + mocker.patch("pathlib.Path.cwd", lambda *_: _path_sub) + + if user_area: + _path.joinpath("level_0").joinpath("simvue.toml").touch() + _path.chmod(stat.S_IXUSR) + _result = sv_util.find_first_instance_of_file("simvue.toml", check_user_space=False) + else: + _path.chmod(stat.S_IXUSR) + _result = sv_util.find_first_instance_of_file("simvue.toml", check_user_space=False) is None + + _path.chmod(stat.S_IRWXU) + assert _result + +@pytest.mark.config +def test_find_first_file_at_root(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture) -> None: + # Deactivate the server checks for this test + monkeypatch.setenv("SIMVUE_NO_SERVER_CHECK", "True") + monkeypatch.delenv("SIMVUE_TOKEN", False) + monkeypatch.delenv("SIMVUE_URL", False) + + @property + def _returns_self(self): + return self + + + with tempfile.TemporaryDirectory() as temp_d: + _path = pathlib.Path(temp_d) + _path_sub = _path.joinpath("level_0") + _path_sub.mkdir() + _path.joinpath("level_0").joinpath("simvue.toml").touch() + + for i in range(1, 5): + _path_sub = _path_sub.joinpath(f"level_{i}") + _path_sub.mkdir() + mocker.patch("pathlib.Path.parent", _returns_self) + mocker.patch("pathlib.Path.cwd", lambda *_: _path_sub) + + assert not sv_util.find_first_instance_of_file("simvue.toml", check_user_space=False) + +