Coverage for tests/test_timer.py: 16%
219 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-01 15:14 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-01 15:14 -0700
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 datetime
23import logging
24import os.path
25import pstats
26import tempfile
27import time
28import unittest
29from dataclasses import dataclass
31from astropy import units as u
32from lsst.utils.timer import logInfo, logPairs, profile, time_this, timeMethod
34log = logging.getLogger("test_timer")
36THIS_FILE = os.path.basename(__file__)
38# Only use this in a single test but needs to be associated
39# with the function up front.
40test_metadata = {}
43@dataclass
44class Example1:
45 """Test class with log and metadata properties, similar to ``Task``."""
47 log: logging.Logger
48 metadata: dict
50 @timeMethod
51 def sleeper(self, duration: float) -> None:
52 """Sleep for some time."""
53 time.sleep(duration)
56@timeMethod
57def decorated_sleeper_nothing(self, duration: float) -> None:
58 """Test function that sleeps."""
59 time.sleep(duration)
62@timeMethod(logger=log)
63def decorated_sleeper_logger(self, duration: float) -> None:
64 """Test function that sleeps and logs."""
65 time.sleep(duration)
68@timeMethod(logger=log, logLevel=logging.INFO)
69def decorated_sleeper_logger_level(self, duration: float) -> None:
70 """Test function that logs at INFO."""
71 time.sleep(duration)
74@timeMethod(metadata=test_metadata)
75def decorated_sleeper_metadata(self, duration: float) -> None:
76 """Test function that uses external metadata."""
77 time.sleep(duration)
80class TestTimeMethod(unittest.TestCase):
81 """Test the time method decorator."""
83 def testLogPairs(self):
84 # Test the non-obj case.
85 logger = logging.getLogger("test")
86 pairs = (("name1", 0), ("name2", 1))
87 metadata = {}
88 with self.assertLogs(level=logging.INFO) as cm:
89 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata)
90 self.assertEqual(len(cm.output), 1, cm.output)
91 self.assertTrue(cm.output[0].endswith("name1=0; name2=1"), cm.output)
92 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
93 self.assertEqual(metadata, {"name1": [0], "name2": [1]})
95 # Call it again with an explicit stack level.
96 # Force it to come from lsst.utils.
97 with self.assertLogs(level=logging.INFO) as cm:
98 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata, stacklevel=0)
99 self.assertEqual(cm.records[0].filename, "timer.py")
101 # Check that the log message is filtered by default.
102 with self.assertLogs(level=logging.INFO) as cm:
103 logPairs(None, pairs, logger=logger, metadata=metadata)
104 logger.info("Message")
105 self.assertEqual(len(cm.records), 1)
107 def testLogInfo(self):
108 metadata = {}
109 logger = logging.getLogger("testLogInfo")
110 with self.assertLogs(level=logging.INFO) as cm:
111 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO)
112 self.assertEqual(cm.records[0].filename, THIS_FILE)
113 self.assertIn("PrefixUtc", metadata)
114 self.assertIn("PrefixMaxResidentSetSize", metadata)
115 self.assertEqual(metadata["__version__"], 1)
117 # Again with no log output.
118 logInfo(None, prefix="Prefix", metadata=metadata)
119 self.assertEqual(len(metadata["PrefixUtc"]), 2)
121 # With an explicit stacklevel.
122 with self.assertLogs(level=logging.INFO) as cm:
123 logInfo(
124 None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO, stacklevel=0
125 )
126 self.assertEqual(cm.records[0].filename, "timer.py")
128 def assertTimer(self, duration, task):
129 # Call it twice to test the "add" functionality.
130 task.sleeper(duration)
131 task.sleeper(duration)
132 counter = 2
134 has_logger = getattr(task, "log", None) is not None and task.log is not None
135 has_metadata = getattr(task, "metadata", None) is not None and task.metadata is not None
137 if has_logger:
138 counter += 1
139 with self.assertLogs("timer.task", level=logging.DEBUG) as cm:
140 task.sleeper(duration)
141 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
143 if has_metadata:
144 self.assertEqual(len(task.metadata["sleeperStartUserTime"]), counter)
146 start = datetime.datetime.fromisoformat(task.metadata["sleeperStartUtc"][1])
147 end = datetime.datetime.fromisoformat(task.metadata["sleeperEndUtc"][1])
148 delta = end - start
149 delta_sec = delta.seconds + (delta.microseconds / 1e6)
150 self.assertGreaterEqual(delta_sec, duration)
152 def testTaskLike(self):
153 """Test timer on something that looks like a Task."""
154 # Call with different parameters.
155 parameters = (
156 (logging.getLogger("task"), {}),
157 (logging.getLogger("task"), None),
158 (None, {}),
159 (None, None),
160 )
162 duration = 0.1
163 for log, metadata in parameters:
164 with self.subTest(log=log, metadata=metadata):
165 task = Example1(log=log, metadata=metadata)
166 self.assertTimer(duration, task)
168 def testDecorated(self):
169 """Test timeMethod on non-Task like instances."""
170 duration = 0.1
172 # The "self" object shouldn't be usable but this should do nothing
173 # and not crash.
174 decorated_sleeper_nothing(self, duration)
176 # Use a function decorated for logging.
177 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
178 decorated_sleeper_logger(self, duration)
179 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
181 # And adjust the log level
182 with self.assertLogs("timer.test_timer", level=logging.INFO):
183 decorated_sleeper_logger_level(self, duration)
185 # Use a function decorated for metadata.
186 self.assertEqual(len(test_metadata), 0)
187 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
188 # Check that we only get a single log message and nothing from
189 # timeMethod itself.
190 decorated_sleeper_metadata(self, duration)
191 logging.getLogger("timer.test_timer").debug("sentinel")
192 self.assertEqual(len(cm.output), 1)
193 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata)
196class TimerTestCase(unittest.TestCase):
197 """Test the timer functionality."""
199 def testTimer(self):
200 with self.assertLogs(level="DEBUG") as cm:
201 with time_this():
202 pass
203 self.assertEqual(cm.records[0].name, "timer")
204 self.assertEqual(cm.records[0].levelname, "DEBUG")
205 self.assertEqual(cm.records[0].filename, THIS_FILE)
207 with self.assertLogs(level="DEBUG") as cm:
208 with time_this(prefix=None):
209 pass
210 self.assertEqual(cm.records[0].name, "root")
211 self.assertEqual(cm.records[0].levelname, "DEBUG")
212 self.assertIn("Took", cm.output[0])
213 self.assertNotIn(": Took", cm.output[0])
214 self.assertNotIn("; ", cm.output[0])
215 self.assertEqual(cm.records[0].filename, THIS_FILE)
217 # Report memory usage.
218 with self.assertLogs(level="DEBUG") as cm:
219 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
220 pass
221 self.assertEqual(cm.records[0].name, "root")
222 self.assertEqual(cm.records[0].levelname, "DEBUG")
223 self.assertIn("Took", cm.output[0])
224 self.assertIn("memory", cm.output[0])
225 self.assertIn("delta", cm.output[0])
226 self.assertIn("peak delta", cm.output[0])
227 self.assertIn("byte", cm.output[0])
229 # Request memory usage but with log level that will not issue it.
230 with self.assertLogs(level="INFO") as cm:
231 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
232 pass
233 # Ensure that a log message is issued.
234 _log = logging.getLogger()
235 _log.info("info")
236 self.assertEqual(cm.records[0].name, "root")
237 self.assertEqual(cm.records[0].levelname, "INFO")
238 all = "\n".join(cm.output)
239 self.assertNotIn("Took", all)
240 self.assertNotIn("memory", all)
242 # Report memory usage including child processes.
243 with self.assertLogs(level="DEBUG") as cm:
244 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_child=True):
245 pass
246 self.assertEqual(cm.records[0].name, "root")
247 self.assertEqual(cm.records[0].levelname, "DEBUG")
248 self.assertIn("Took", cm.output[0])
249 self.assertIn("memory", cm.output[0])
250 self.assertIn("delta", cm.output[0])
251 self.assertIn("peak delta", cm.output[0])
252 self.assertIn("byte", cm.output[0])
254 # Report memory usage, use non-default, but a valid memory unit.
255 with self.assertLogs(level="DEBUG") as cm:
256 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.kilobyte):
257 pass
258 self.assertEqual(cm.records[0].name, "root")
259 self.assertEqual(cm.records[0].levelname, "DEBUG")
260 self.assertIn("Took", cm.output[0])
261 self.assertIn("memory", cm.output[0])
262 self.assertIn("delta", cm.output[0])
263 self.assertIn("peak delta", cm.output[0])
264 self.assertIn("kbyte", cm.output[0])
266 # Report memory usage, use an invalid memory unit.
267 with self.assertLogs(level="DEBUG") as cm:
268 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.gram):
269 pass
270 self.assertEqual(cm.records[0].name, "lsst.utils.timer")
271 self.assertEqual(cm.records[0].levelname, "WARNING")
272 self.assertIn("Invalid", cm.output[0])
273 self.assertIn("byte", cm.output[0])
274 self.assertEqual(cm.records[1].name, "root")
275 self.assertEqual(cm.records[1].levelname, "DEBUG")
276 self.assertIn("Took", cm.output[1])
277 self.assertIn("memory", cm.output[1])
278 self.assertIn("delta", cm.output[1])
279 self.assertIn("peak delta", cm.output[1])
280 self.assertIn("byte", cm.output[1])
282 # Change logging level
283 with self.assertLogs(level="INFO") as cm:
284 with time_this(level=logging.INFO, prefix=None):
285 pass
286 self.assertEqual(cm.records[0].name, "root")
287 self.assertIn("Took", cm.output[0])
288 self.assertIn("seconds", cm.output[0])
290 # Use a new logger with a message.
291 msg = "Test message %d"
292 test_num = 42
293 logname = "test"
294 with self.assertLogs(level="DEBUG") as cm:
295 with time_this(log=logging.getLogger(logname), msg=msg, args=(42,), prefix=None):
296 pass
297 self.assertEqual(cm.records[0].name, logname)
298 self.assertIn("Took", cm.output[0])
299 self.assertIn(msg % test_num, cm.output[0])
301 # Prefix the logger.
302 prefix = "prefix"
303 with self.assertLogs(level="DEBUG") as cm:
304 with time_this(prefix=prefix):
305 pass
306 self.assertEqual(cm.records[0].name, prefix)
307 self.assertIn("Took", cm.output[0])
309 # Prefix explicit logger.
310 with self.assertLogs(level="DEBUG") as cm:
311 with time_this(log=logging.getLogger(logname), prefix=prefix):
312 pass
313 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
315 # Trigger a problem.
316 with self.assertLogs(level="ERROR") as cm:
317 with self.assertRaises(RuntimeError):
318 with time_this(log=logging.getLogger(logname), prefix=prefix):
319 raise RuntimeError("A problem")
320 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
321 self.assertEqual(cm.records[0].levelname, "ERROR")
324class ProfileTestCase(unittest.TestCase):
325 """Test profiling decorator."""
327 def test_profile(self):
328 logger = logging.getLogger("profile")
330 with profile(None) as prof:
331 pass
332 self.assertIsNone(prof)
334 with tempfile.NamedTemporaryFile() as tmp:
335 with self.assertLogs("profile", level=logging.INFO) as cm:
336 with profile(tmp.name, logger) as prof:
337 pass
338 self.assertEqual(len(cm.output), 2)
339 self.assertIsNotNone(prof)
340 self.assertTrue(os.path.exists(tmp.name))
341 self.assertIsInstance(pstats.Stats(tmp.name), pstats.Stats)
344if __name__ == "__main__":
345 unittest.main()