Coverage for tests/test_file.py: 26%
146 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-24 01:20 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-24 01:20 -0800
1# This file is part of lsst-resources.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12import contextlib
13import os
14import pathlib
15import unittest
16import unittest.mock
17import urllib.parse
19from lsst.resources import ResourcePath, ResourcePathExpression
20from lsst.resources.tests import GenericReadWriteTestCase, GenericTestCase
22TESTDIR = os.path.abspath(os.path.dirname(__file__))
25class SimpleTestCase(unittest.TestCase):
26 """Basic tests for file URIs."""
28 def test_instance(self):
29 for example in (
30 "xxx",
31 ResourcePath("xxx"),
32 pathlib.Path("xxx"),
33 urllib.parse.urlparse("file:///xxx"),
34 ):
35 self.assertIsInstance(example, ResourcePathExpression)
37 for example in ({1, 2, 3}, 42, self):
38 self.assertNotIsInstance(example, ResourcePathExpression)
41class FileTestCase(GenericTestCase, unittest.TestCase):
42 """File-specific generic test cases."""
44 scheme = "file"
45 netloc = "localhost"
47 def test_env_var(self):
48 """Test that environment variables are expanded."""
49 with unittest.mock.patch.dict(os.environ, {"MY_TEST_DIR": "/a/b/c"}):
50 uri = ResourcePath("${MY_TEST_DIR}/d.txt")
51 self.assertEqual(uri.path, "/a/b/c/d.txt")
52 self.assertEqual(uri.scheme, "file")
54 # This will not expand
55 uri = ResourcePath("${MY_TEST_DIR}/d.txt", forceAbsolute=False)
56 self.assertEqual(uri.path, "${MY_TEST_DIR}/d.txt")
57 self.assertFalse(uri.scheme)
59 def test_ospath(self):
60 """File URIs have ospath property."""
61 file = ResourcePath(self._make_uri("a/test.txt"))
62 self.assertEqual(file.ospath, "/a/test.txt")
63 self.assertEqual(file.ospath, file.path)
65 # A Schemeless URI can take unquoted files but will be quoted
66 # when it becomes a file URI.
67 something = "/a#/???.txt"
68 file = ResourcePath(something, forceAbsolute=True)
69 self.assertEqual(file.scheme, "file")
70 self.assertEqual(file.ospath, something, "From URI: {file}")
71 self.assertNotIn("???", file.path)
73 def test_path_lib(self):
74 """File URIs can be created from pathlib."""
75 file = ResourcePath(self._make_uri("a/test.txt"))
77 path_file = pathlib.Path(file.ospath)
78 from_path = ResourcePath(path_file)
79 self.assertEqual(from_path.ospath, file.ospath)
81 def test_schemeless_root(self):
82 root = ResourcePath(self._make_uri("/root"))
83 via_root = ResourcePath("b.txt", root=root)
84 self.assertEqual(via_root.ospath, "/root/b.txt")
87TEST_UMASK = 0o0333
90class FileReadWriteTestCase(GenericReadWriteTestCase, unittest.TestCase):
91 """File tests involving reading and writing of data."""
93 scheme = "file"
94 netloc = "localhost"
95 testdir = TESTDIR
96 transfer_modes = ("move", "copy", "link", "hardlink", "symlink", "relsymlink")
98 def test_transfer_identical(self):
99 """Test overwrite of identical files.
101 Only relevant for local files.
102 """
103 dir1 = self.tmpdir.join("dir1", forceDirectory=True)
104 dir1.mkdir()
105 self.assertTrue(dir1.exists())
106 dir2 = self.tmpdir.join("dir2", forceDirectory=True)
107 # A symlink can't include a trailing slash.
108 dir2_ospath = dir2.ospath
109 if dir2_ospath.endswith("/"):
110 dir2_ospath = dir2_ospath[:-1]
111 os.symlink(dir1.ospath, dir2_ospath)
113 # Write a test file.
114 src_file = dir1.join("test.txt")
115 content = "0123456"
116 src_file.write(content.encode())
118 # Construct URI to destination that should be identical.
119 dest_file = dir2.join("test.txt")
120 self.assertTrue(dest_file.exists())
121 self.assertNotEqual(src_file, dest_file)
123 # Transfer it over itself.
124 dest_file.transfer_from(src_file, transfer="symlink", overwrite=True)
125 new_content = dest_file.read().decode()
126 self.assertEqual(content, new_content)
128 def test_local_temporary(self):
129 """Create temporary local file if no prefix specified."""
130 with ResourcePath.temporary_uri(suffix=".json") as tmp:
131 self.assertEqual(tmp.getExtension(), ".json", f"uri: {tmp}")
132 self.assertTrue(tmp.isabs(), f"uri: {tmp}")
133 self.assertFalse(tmp.exists(), f"uri: {tmp}")
134 tmp.write(b"abcd")
135 self.assertTrue(tmp.exists(), f"uri: {tmp}")
136 self.assertTrue(tmp.isTemporary)
137 self.assertTrue(tmp.isLocal)
139 # If we now ask for a local form of this temporary file
140 # it should still be temporary and it should not be deleted
141 # on exit.
142 with tmp.as_local() as loc:
143 self.assertEqual(tmp, loc)
144 self.assertTrue(loc.isTemporary)
145 self.assertTrue(tmp.exists())
146 self.assertFalse(tmp.exists(), f"uri: {tmp}")
148 with ResourcePath.temporary_uri(suffix=".yaml", delete=False) as tmp:
149 tmp.write(b"1234")
150 self.assertTrue(tmp.exists(), f"uri: {tmp}")
151 # If the file doesn't exist there is nothing to clean up so a failure
152 # here is not a problem.
153 self.assertTrue(tmp.exists(), f"uri: {tmp} should still exist")
155 # If removal does not work it's worth reporting that as an error.
156 tmp.remove()
158 def test_transfers_from_local(self):
159 """Extra tests for local transfers."""
160 target = self.tmpdir.join("a/target.txt")
161 with ResourcePath.temporary_uri() as tmp:
162 tmp.write(b"")
163 self.assertTrue(tmp.isTemporary)
165 # Symlink transfers for temporary resources should
166 # trigger a debug message.
167 for transfer in ("symlink", "relsymlink"):
168 with self.assertLogs("lsst.resources", level="DEBUG") as cm:
169 target.transfer_from(tmp, transfer)
170 target.remove()
171 self.assertIn("Using a symlink for a temporary", "".join(cm.output))
173 # Force the target directory to be created.
174 target.transfer_from(tmp, "move")
175 self.assertFalse(tmp.exists())
177 # Temporary file now gone so transfer should not work.
178 with self.assertRaises(FileNotFoundError):
179 target.transfer_from(tmp, "move", overwrite=True)
181 def test_write_with_restrictive_umask(self):
182 self._test_file_with_restrictive_umask(lambda target: target.write(b"123"))
184 def test_transfer_from_with_restrictive_umask(self):
185 def cb(target):
186 with ResourcePath.temporary_uri() as tmp:
187 tmp.write(b"")
188 target.transfer_from(tmp, "copy")
190 self._test_file_with_restrictive_umask(cb)
192 def test_mkdir_with_restrictive_umask(self):
193 self._test_with_restrictive_umask(lambda target: target.mkdir())
195 def test_temporary_uri_with_restrictive_umask(self):
196 with _override_umask(TEST_UMASK):
197 with ResourcePath.temporary_uri() as tmp:
198 tmp.write(b"")
199 self.assertTrue(tmp.exists())
201 def _test_file_with_restrictive_umask(self, callback):
202 def inner_cb(target):
203 callback(target)
205 # Make sure the umask was respected for the file itself
206 file_mode = os.stat(target.ospath).st_mode
207 self.assertEqual(file_mode & TEST_UMASK, 0)
209 self._test_with_restrictive_umask(inner_cb)
211 def _test_with_restrictive_umask(self, callback):
212 """Make sure that parent directories for a file can be created even if
213 the user has set a process umask that restricts the write and traverse
214 bits.
215 """
216 with _override_umask(TEST_UMASK):
217 target = self.tmpdir.join("a/b/target.txt")
218 callback(target)
219 self.assertTrue(target.exists())
221 dir_b_path = os.path.dirname(target.ospath)
222 dir_a_path = os.path.dirname(dir_b_path)
223 for dir in [dir_a_path, dir_b_path]:
224 # Make sure we only added the minimum permissions needed for it
225 # to work (owner-write and owner-traverse)
226 mode = os.stat(dir).st_mode
227 self.assertEqual(mode & TEST_UMASK, 0o0300, f"Permissions incorrect for {dir}: {mode:o}")
229 @unittest.mock.patch("lsst.resources._resourcePath._POOL_EXECUTOR_CLASS", None)
230 @unittest.mock.patch.dict(os.environ, {"LSST_RESOURCES_EXECUTOR": "process"})
231 def test_mexists_process(self) -> None:
232 """Test mexists with override executor pool.
234 Force test with process pool.
235 """
236 super().test_mexists()
238 @unittest.mock.patch("lsst.resources._resourcePath._POOL_EXECUTOR_CLASS", None)
239 @unittest.mock.patch.dict(os.environ, {"LSST_RESOURCES_EXECUTOR": "process"})
240 def test_mtransfer_process(self) -> None:
241 """Test transfer with override executor pool.
243 Force test with process pool.
244 """
245 super().test_mtransfer()
248@contextlib.contextmanager
249def _override_umask(temp_umask):
250 old = os.umask(temp_umask)
251 try:
252 yield
253 finally:
254 os.umask(old)
257if __name__ == "__main__":
258 unittest.main()