Coverage for tests/test_timer.py: 13%
219 statements
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-29 02:53 -0700
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-29 02:53 -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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12import datetime
13import logging
14import os.path
15import pstats
16import tempfile
17import time
18import unittest
19from dataclasses import dataclass
21from astropy import units as u
22from lsst.utils.timer import logInfo, logPairs, profile, time_this, timeMethod
24log = logging.getLogger("test_timer")
26THIS_FILE = os.path.basename(__file__)
28# Only use this in a single test but needs to be associated
29# with the function up front.
30test_metadata = {}
33@dataclass
34class Example1:
35 log: logging.Logger
36 metadata: dict
38 @timeMethod
39 def sleeper(self, duration: float) -> None:
40 """Sleep for some time."""
41 time.sleep(duration)
44@timeMethod
45def decorated_sleeper_nothing(self, duration: float) -> None:
46 time.sleep(duration)
49@timeMethod(logger=log)
50def decorated_sleeper_logger(self, duration: float) -> None:
51 time.sleep(duration)
54@timeMethod(logger=log, logLevel=logging.INFO)
55def decorated_sleeper_logger_level(self, duration: float) -> None:
56 time.sleep(duration)
59@timeMethod(metadata=test_metadata)
60def decorated_sleeper_metadata(self, duration: float) -> None:
61 time.sleep(duration)
64class TestTimeMethod(unittest.TestCase):
65 def testLogPairs(self):
66 # Test the non-obj case.
67 logger = logging.getLogger("test")
68 pairs = (("name1", 0), ("name2", 1))
69 metadata = {}
70 with self.assertLogs(level=logging.INFO) as cm:
71 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata)
72 self.assertEqual(len(cm.output), 1, cm.output)
73 self.assertTrue(cm.output[0].endswith("name1=0; name2=1"), cm.output)
74 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
75 self.assertEqual(metadata, {"name1": [0], "name2": [1]})
77 # Call it again with an explicit stack level.
78 # Force it to come from lsst.utils.
79 with self.assertLogs(level=logging.INFO) as cm:
80 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata, stacklevel=0)
81 self.assertEqual(cm.records[0].filename, "timer.py")
83 # Check that the log message is filtered by default.
84 with self.assertLogs(level=logging.INFO) as cm:
85 logPairs(None, pairs, logger=logger, metadata=metadata)
86 logger.info("Message")
87 self.assertEqual(len(cm.records), 1)
89 def testLogInfo(self):
90 metadata = {}
91 logger = logging.getLogger("testLogInfo")
92 with self.assertLogs(level=logging.INFO) as cm:
93 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO)
94 self.assertEqual(cm.records[0].filename, THIS_FILE)
95 self.assertIn("PrefixUtc", metadata)
96 self.assertIn("PrefixMaxResidentSetSize", metadata)
97 self.assertEqual(metadata["__version__"], 1)
99 # Again with no log output.
100 logInfo(None, prefix="Prefix", metadata=metadata)
101 self.assertEqual(len(metadata["PrefixUtc"]), 2)
103 # With an explicit stacklevel.
104 with self.assertLogs(level=logging.INFO) as cm:
105 logInfo(
106 None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO, stacklevel=0
107 )
108 self.assertEqual(cm.records[0].filename, "timer.py")
110 def assertTimer(self, duration, task):
111 # Call it twice to test the "add" functionality.
112 task.sleeper(duration)
113 task.sleeper(duration)
114 counter = 2
116 has_logger = getattr(task, "log", None) is not None and task.log is not None
117 has_metadata = getattr(task, "metadata", None) is not None and task.metadata is not None
119 if has_logger:
120 counter += 1
121 with self.assertLogs("timer.task", level=logging.DEBUG) as cm:
122 task.sleeper(duration)
123 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
125 if has_metadata:
126 self.assertEqual(len(task.metadata["sleeperStartUserTime"]), counter)
128 start = datetime.datetime.fromisoformat(task.metadata["sleeperStartUtc"][1])
129 end = datetime.datetime.fromisoformat(task.metadata["sleeperEndUtc"][1])
130 delta = end - start
131 delta_sec = delta.seconds + (delta.microseconds / 1e6)
132 self.assertGreaterEqual(delta_sec, duration)
134 def testTaskLike(self):
135 """Test timer on something that looks like a Task."""
137 # Call with different parameters.
138 parameters = (
139 (logging.getLogger("task"), {}),
140 (logging.getLogger("task"), None),
141 (None, {}),
142 (None, None),
143 )
145 duration = 0.1
146 for log, metadata in parameters:
147 with self.subTest(log=log, metadata=metadata):
148 task = Example1(log=log, metadata=metadata)
149 self.assertTimer(duration, task)
151 def testDecorated(self):
152 """Test timeMethod on non-Task like instances."""
153 duration = 0.1
155 # The "self" object shouldn't be usable but this should do nothing
156 # and not crash.
157 decorated_sleeper_nothing(self, duration)
159 # Use a function decorated for logging.
160 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
161 decorated_sleeper_logger(self, duration)
162 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
164 # And adjust the log level
165 with self.assertLogs("timer.test_timer", level=logging.INFO):
166 decorated_sleeper_logger_level(self, duration)
168 # Use a function decorated for metadata.
169 self.assertEqual(len(test_metadata), 0)
170 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
171 # Check that we only get a single log message and nothing from
172 # timeMethod itself.
173 decorated_sleeper_metadata(self, duration)
174 logging.getLogger("timer.test_timer").debug("sentinel")
175 self.assertEqual(len(cm.output), 1)
176 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata)
179class TimerTestCase(unittest.TestCase):
180 def testTimer(self):
181 with self.assertLogs(level="DEBUG") as cm:
182 with time_this():
183 pass
184 self.assertEqual(cm.records[0].name, "timer")
185 self.assertEqual(cm.records[0].levelname, "DEBUG")
186 self.assertEqual(cm.records[0].filename, THIS_FILE)
188 with self.assertLogs(level="DEBUG") as cm:
189 with time_this(prefix=None):
190 pass
191 self.assertEqual(cm.records[0].name, "root")
192 self.assertEqual(cm.records[0].levelname, "DEBUG")
193 self.assertIn("Took", cm.output[0])
194 self.assertNotIn(": Took", cm.output[0])
195 self.assertNotIn("; ", cm.output[0])
196 self.assertEqual(cm.records[0].filename, THIS_FILE)
198 # Report memory usage.
199 with self.assertLogs(level="DEBUG") as cm:
200 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
201 pass
202 self.assertEqual(cm.records[0].name, "root")
203 self.assertEqual(cm.records[0].levelname, "DEBUG")
204 self.assertIn("Took", cm.output[0])
205 self.assertIn("memory", cm.output[0])
206 self.assertIn("delta", cm.output[0])
207 self.assertIn("peak delta", cm.output[0])
208 self.assertIn("byte", cm.output[0])
210 # Request memory usage but with log level that will not issue it.
211 with self.assertLogs(level="INFO") as cm:
212 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
213 pass
214 # Ensure that a log message is issued.
215 _log = logging.getLogger()
216 _log.info("info")
217 self.assertEqual(cm.records[0].name, "root")
218 self.assertEqual(cm.records[0].levelname, "INFO")
219 all = "\n".join(cm.output)
220 self.assertNotIn("Took", all)
221 self.assertNotIn("memory", all)
223 # Report memory usage including child processes.
224 with self.assertLogs(level="DEBUG") as cm:
225 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_child=True):
226 pass
227 self.assertEqual(cm.records[0].name, "root")
228 self.assertEqual(cm.records[0].levelname, "DEBUG")
229 self.assertIn("Took", cm.output[0])
230 self.assertIn("memory", cm.output[0])
231 self.assertIn("delta", cm.output[0])
232 self.assertIn("peak delta", cm.output[0])
233 self.assertIn("byte", cm.output[0])
235 # Report memory usage, use non-default, but a valid memory unit.
236 with self.assertLogs(level="DEBUG") as cm:
237 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.kilobyte):
238 pass
239 self.assertEqual(cm.records[0].name, "root")
240 self.assertEqual(cm.records[0].levelname, "DEBUG")
241 self.assertIn("Took", cm.output[0])
242 self.assertIn("memory", cm.output[0])
243 self.assertIn("delta", cm.output[0])
244 self.assertIn("peak delta", cm.output[0])
245 self.assertIn("kbyte", cm.output[0])
247 # Report memory usage, use an invalid memory unit.
248 with self.assertLogs(level="DEBUG") as cm:
249 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.gram):
250 pass
251 self.assertEqual(cm.records[0].name, "lsst.utils.timer")
252 self.assertEqual(cm.records[0].levelname, "WARNING")
253 self.assertIn("Invalid", cm.output[0])
254 self.assertIn("byte", cm.output[0])
255 self.assertEqual(cm.records[1].name, "root")
256 self.assertEqual(cm.records[1].levelname, "DEBUG")
257 self.assertIn("Took", cm.output[1])
258 self.assertIn("memory", cm.output[1])
259 self.assertIn("delta", cm.output[1])
260 self.assertIn("peak delta", cm.output[1])
261 self.assertIn("byte", cm.output[1])
263 # Change logging level
264 with self.assertLogs(level="INFO") as cm:
265 with time_this(level=logging.INFO, prefix=None):
266 pass
267 self.assertEqual(cm.records[0].name, "root")
268 self.assertIn("Took", cm.output[0])
269 self.assertIn("seconds", cm.output[0])
271 # Use a new logger with a message.
272 msg = "Test message %d"
273 test_num = 42
274 logname = "test"
275 with self.assertLogs(level="DEBUG") as cm:
276 with time_this(log=logging.getLogger(logname), msg=msg, args=(42,), prefix=None):
277 pass
278 self.assertEqual(cm.records[0].name, logname)
279 self.assertIn("Took", cm.output[0])
280 self.assertIn(msg % test_num, cm.output[0])
282 # Prefix the logger.
283 prefix = "prefix"
284 with self.assertLogs(level="DEBUG") as cm:
285 with time_this(prefix=prefix):
286 pass
287 self.assertEqual(cm.records[0].name, prefix)
288 self.assertIn("Took", cm.output[0])
290 # Prefix explicit logger.
291 with self.assertLogs(level="DEBUG") as cm:
292 with time_this(log=logging.getLogger(logname), prefix=prefix):
293 pass
294 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
296 # Trigger a problem.
297 with self.assertLogs(level="ERROR") as cm:
298 with self.assertRaises(RuntimeError):
299 with time_this(log=logging.getLogger(logname), prefix=prefix):
300 raise RuntimeError("A problem")
301 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
302 self.assertEqual(cm.records[0].levelname, "ERROR")
305class ProfileTestCase(unittest.TestCase):
306 def test_profile(self):
307 logger = logging.getLogger("profile")
309 with profile(None) as prof:
310 pass
311 self.assertIsNone(prof)
313 with tempfile.NamedTemporaryFile() as tmp:
314 with self.assertLogs("profile", level=logging.INFO) as cm:
315 with profile(tmp.name, logger) as prof:
316 pass
317 self.assertEqual(len(cm.output), 2)
318 self.assertIsNotNone(prof)
319 self.assertTrue(os.path.exists(tmp.name))
320 self.assertIsInstance(pstats.Stats(tmp.name), pstats.Stats),
323if __name__ == "__main__":
324 unittest.main()