Coverage for tests/test_threads.py: 83%
62 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 08:43 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 08:43 +0000
1# This file is part of utils.
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# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import os
23import unittest
24import unittest.mock
26from lsst.utils.threads import disable_implicit_threading, set_thread_envvars
28try:
29 import numexpr
30except ImportError:
31 numexpr = None
32try:
33 import threadpoolctl
34except ImportError:
35 threadpoolctl = None
36try:
37 import pyarrow
38except ImportError:
39 pyarrow = None
40try:
41 import numba
42except ImportError:
43 numba = None
46class ThreadsTestCase(unittest.TestCase):
47 """Tests for threads."""
49 def testDisable(self):
50 set_thread_envvars(2, override=True)
51 self.assertEqual(os.environ["OMP_NUM_THREADS"], "2")
52 set_thread_envvars(3, override=False)
53 self.assertEqual(os.environ["OMP_NUM_THREADS"], "2")
55 disable_implicit_threading()
56 self.assertEqual(os.environ["OMP_NUM_THREADS"], "1")
57 self.assertEqual(os.environ["OMP_PROC_BIND"], "false")
59 # Check that we have only one thread.
60 if numexpr: 60 ↛ 62line 60 didn't jump to line 62 because the condition on line 60 was always true
61 self.assertEqual(numexpr.utils.get_num_threads(), 1)
62 if threadpoolctl: 62 ↛ exitline 62 didn't return from function 'testDisable' because the condition on line 62 was always true
63 info = threadpoolctl.threadpool_info()
64 for api in info:
65 self.assertEqual(api["num_threads"], 1, f"API: {api}")
67 def testEnvVarsForEnvOnlyLibraries(self):
68 """Libraries that are only controllable via environment variables
69 must be included in the list of variables that are set.
70 """
71 set_thread_envvars(2, override=True)
72 for var in (
73 "NUMBA_NUM_THREADS",
74 "ARROW_IO_THREADS",
75 "VECLIB_MAXIMUM_THREADS",
76 "RAYON_NUM_THREADS",
77 "POLARS_MAX_THREADS",
78 "BLIS_NUM_THREADS",
79 ):
80 self.assertEqual(os.environ.get(var), "2", f"Variable: {var}")
82 @unittest.skipIf(pyarrow is None, "pyarrow is not available")
83 def testDisablePyarrow(self):
84 """Already-created pyarrow thread pools must be resized since they
85 do not react to environment variables after creation.
86 """
87 pyarrow.set_cpu_count(4)
88 pyarrow.set_io_thread_count(4)
89 disable_implicit_threading()
90 self.assertEqual(pyarrow.cpu_count(), 1)
91 self.assertEqual(pyarrow.io_thread_count(), 1)
93 @unittest.skipIf(numba is None, "numba is not available")
94 def testDisableNumba(self):
95 """An already-imported numba must be limited to a single thread, and
96 jit compilation afterwards must still work.
97 """
98 if numba.config.NUMBA_NUM_THREADS > 1: 98 ↛ 100line 98 didn't jump to line 100 because the condition on line 98 was always true
99 numba.set_num_threads(2)
100 disable_implicit_threading()
101 self.assertEqual(numba.get_num_threads(), 1)
103 # numba re-reads NUMBA_NUM_THREADS every time it reloads its
104 # configuration, which happens on each jit compilation. Compiling a
105 # function must not raise because the environment variable and numba's
106 # recorded thread count disagree.
107 @numba.njit("float64(float64)")
108 def add_one(x: float) -> float:
109 return x + 1.0
111 self.assertEqual(add_one(1.0), 2.0)
112 self.assertEqual(numba.get_num_threads(), 1)
114 @unittest.skipIf(threadpoolctl is None, "threadpoolctl is not available")
115 def testMissingThreadpoolctlWarning(self):
116 """Absence of threadpoolctl silently weakens the thread disabling
117 so it must be reported.
118 """
119 with (
120 unittest.mock.patch("lsst.utils.threads.threadpool_limits", None),
121 self.assertLogs("lsst.utils.threads", "WARNING") as cm,
122 ):
123 disable_implicit_threading()
124 self.assertIn("threadpoolctl", "\n".join(cm.output))
127if __name__ == "__main__":
128 unittest.main()