From a068280a87b84c59fd47b1e260dbad6f530ce18f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 01:47:22 +0000 Subject: [PATCH] grass.pygrass: Test that closing a map opened for update releases the sidx file Throwaway check that the test fails without the library fix. --- .../tests/grass_pygrass_vector_sidx_checks.py | 49 +++++++++++++++++++ .../tests/grass_pygrass_vector_sidx_test.py | 47 ++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_checks.py create mode 100644 python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_test.py diff --git a/python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_checks.py b/python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_checks.py new file mode 100644 index 00000000000..b50e033d3db --- /dev/null +++ b/python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_checks.py @@ -0,0 +1,49 @@ +# Copyright (C) 2026 by the GRASS Development Team +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Open a vector map in update mode, close it, and check for a leaked sidx handle + +Run in a GRASS session by ``grass_pygrass_vector_sidx_test.py`` in a +subprocess, because the vector library state is per process. +""" + +import os +import sys +from pathlib import Path + +from grass.pygrass.vector import VectorTopo + + +def sidx_is_open(path): + """Return True if this process still has the file open""" + if sys.platform.startswith("linux"): + # A handle to a replaced file is reported with a " (deleted)" suffix. + return any( + os.path.realpath(fd).startswith(path) + for fd in Path("/proc/self/fd").iterdir() + ) + # Windows refuses to delete an open file. + try: + os.remove(path) + except PermissionError: + return True + return False + + +def main(): + """Create a map, reopen it in update mode, and check the sidx file""" + vect = VectorTopo("sidx_map") + vect.open("w") + vect.close() + vect.open("rw") + vect.close() + mapset_path = os.environ["MAPSET_PATH"] + path = os.path.realpath(os.path.join(mapset_path, "vector", "sidx_map", "sidx")) + if sidx_is_open(path): + print(f"The file {path} was left open after closing the map") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_test.py b/python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_test.py new file mode 100644 index 00000000000..ec3c9d98849 --- /dev/null +++ b/python/grass/pygrass/vector/tests/grass_pygrass_vector_sidx_test.py @@ -0,0 +1,47 @@ +# Copyright (C) 2026 by the GRASS Development Team +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Test that closing a vector map opened in update mode releases the sidx file + +An open file handle left behind makes the removal of the map fail on Windows +and leaks a file descriptor on all platforms. +""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +import grass.script as gs + +CHECKS = Path(__file__).with_name("grass_pygrass_vector_sidx_checks.py") + + +@pytest.fixture(scope="module") +def session(tmp_path_factory): + """Active session in an XY project (scope: module)""" + project = tmp_path_factory.mktemp("sidx") / "project" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + yield session + + +def test_update_mode_releases_sidx(session): + """The sidx file is closed after a map opened with "rw" is closed""" + env = session.env.copy() + env["MAPSET_PATH"] = str( + Path(gs.gisenv(env=session.env)["GISDBASE"]) + / gs.gisenv(env=session.env)["LOCATION_NAME"] + / gs.gisenv(env=session.env)["MAPSET"] + ) + result = subprocess.run( + [sys.executable, str(CHECKS)], + env=env, + capture_output=True, + text=True, + check=False, + timeout=120, + ) + assert result.returncode == 0, f"{result.stdout}\n{result.stderr}"