Coverage for tests/test_timer.py: 16%
219 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-25 09:27 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-25 09:27 +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# 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 """Test class with log and metadata properties, similar to ``Task``."""
37 log: logging.Logger
38 metadata: dict
40 @timeMethod
41 def sleeper(self, duration: float) -> None:
42 """Sleep for some time."""
43 time.sleep(duration)
46@timeMethod
47def decorated_sleeper_nothing(self, duration: float) -> None:
48 """Test function that sleeps."""
49 time.sleep(duration)
52@timeMethod(logger=log)
53def decorated_sleeper_logger(self, duration: float) -> None:
54 """Test function that sleeps and logs."""
55 time.sleep(duration)
58@timeMethod(logger=log, logLevel=logging.INFO)
59def decorated_sleeper_logger_level(self, duration: float) -> None:
60 """Test function that logs at INFO."""
61 time.sleep(duration)
64@timeMethod(metadata=test_metadata)
65def decorated_sleeper_metadata(self, duration: float) -> None:
66 """Test function that uses external metadata."""
67 time.sleep(duration)
70class TestTimeMethod(unittest.TestCase):
71 """Test the time method decorator."""
73 def testLogPairs(self):
74 # Test the non-obj case.
75 logger = logging.getLogger("test")
76 pairs = (("name1", 0), ("name2", 1))
77 metadata = {}
78 with self.assertLogs(level=logging.INFO) as cm:
79 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata)
80 self.assertEqual(len(cm.output), 1, cm.output)
81 self.assertTrue(cm.output[0].endswith("name1=0; name2=1"), cm.output)
82 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
83 self.assertEqual(metadata, {"name1": [0], "name2": [1]})
85 # Call it again with an explicit stack level.
86 # Force it to come from lsst.utils.
87 with self.assertLogs(level=logging.INFO) as cm:
88 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata, stacklevel=0)
89 self.assertEqual(cm.records[0].filename, "timer.py")
91 # Check that the log message is filtered by default.
92 with self.assertLogs(level=logging.INFO) as cm:
93 logPairs(None, pairs, logger=logger, metadata=metadata)
94 logger.info("Message")
95 self.assertEqual(len(cm.records), 1)
97 def testLogInfo(self):
98 metadata = {}
99 logger = logging.getLogger("testLogInfo")
100 with self.assertLogs(level=logging.INFO) as cm:
101 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO)
102 self.assertEqual(cm.records[0].filename, THIS_FILE)
103 self.assertIn("PrefixUtc", metadata)
104 self.assertIn("PrefixMaxResidentSetSize", metadata)
105 self.assertEqual(metadata["__version__"], 1)
107 # Again with no log output.
108 logInfo(None, prefix="Prefix", metadata=metadata)
109 self.assertEqual(len(metadata["PrefixUtc"]), 2)
111 # With an explicit stacklevel.
112 with self.assertLogs(level=logging.INFO) as cm:
113 logInfo(
114 None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO, stacklevel=0
115 )
116 self.assertEqual(cm.records[0].filename, "timer.py")
118 def assertTimer(self, duration, task):
119 # Call it twice to test the "add" functionality.
120 task.sleeper(duration)
121 task.sleeper(duration)
122 counter = 2
124 has_logger = getattr(task, "log", None) is not None and task.log is not None
125 has_metadata = getattr(task, "metadata", None) is not None and task.metadata is not None
127 if has_logger:
128 counter += 1
129 with self.assertLogs("timer.task", level=logging.DEBUG) as cm:
130 task.sleeper(duration)
131 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
133 if has_metadata:
134 self.assertEqual(len(task.metadata["sleeperStartUserTime"]), counter)
136 start = datetime.datetime.fromisoformat(task.metadata["sleeperStartUtc"][1])
137 end = datetime.datetime.fromisoformat(task.metadata["sleeperEndUtc"][1])
138 delta = end - start
139 delta_sec = delta.seconds + (delta.microseconds / 1e6)
140 self.assertGreaterEqual(delta_sec, duration)
142 def testTaskLike(self):
143 """Test timer on something that looks like a Task."""
144 # Call with different parameters.
145 parameters = (
146 (logging.getLogger("task"), {}),
147 (logging.getLogger("task"), None),
148 (None, {}),
149 (None, None),
150 )
152 duration = 0.1
153 for log, metadata in parameters:
154 with self.subTest(log=log, metadata=metadata):
155 task = Example1(log=log, metadata=metadata)
156 self.assertTimer(duration, task)
158 def testDecorated(self):
159 """Test timeMethod on non-Task like instances."""
160 duration = 0.1
162 # The "self" object shouldn't be usable but this should do nothing
163 # and not crash.
164 decorated_sleeper_nothing(self, duration)
166 # Use a function decorated for logging.
167 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
168 decorated_sleeper_logger(self, duration)
169 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
171 # And adjust the log level
172 with self.assertLogs("timer.test_timer", level=logging.INFO):
173 decorated_sleeper_logger_level(self, duration)
175 # Use a function decorated for metadata.
176 self.assertEqual(len(test_metadata), 0)
177 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
178 # Check that we only get a single log message and nothing from
179 # timeMethod itself.
180 decorated_sleeper_metadata(self, duration)
181 logging.getLogger("timer.test_timer").debug("sentinel")
182 self.assertEqual(len(cm.output), 1)
183 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata)
186class TimerTestCase(unittest.TestCase):
187 """Test the timer functionality."""
189 def testTimer(self):
190 with self.assertLogs(level="DEBUG") as cm:
191 with time_this():
192 pass
193 self.assertEqual(cm.records[0].name, "timer")
194 self.assertEqual(cm.records[0].levelname, "DEBUG")
195 self.assertEqual(cm.records[0].filename, THIS_FILE)
197 with self.assertLogs(level="DEBUG") as cm:
198 with time_this(prefix=None):
199 pass
200 self.assertEqual(cm.records[0].name, "root")
201 self.assertEqual(cm.records[0].levelname, "DEBUG")
202 self.assertIn("Took", cm.output[0])
203 self.assertNotIn(": Took", cm.output[0])
204 self.assertNotIn("; ", cm.output[0])
205 self.assertEqual(cm.records[0].filename, THIS_FILE)
207 # Report memory usage.
208 with self.assertLogs(level="DEBUG") as cm:
209 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
210 pass
211 self.assertEqual(cm.records[0].name, "root")
212 self.assertEqual(cm.records[0].levelname, "DEBUG")
213 self.assertIn("Took", cm.output[0])
214 self.assertIn("memory", cm.output[0])
215 self.assertIn("delta", cm.output[0])
216 self.assertIn("peak delta", cm.output[0])
217 self.assertIn("byte", cm.output[0])
219 # Request memory usage but with log level that will not issue it.
220 with self.assertLogs(level="INFO") as cm:
221 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
222 pass
223 # Ensure that a log message is issued.
224 _log = logging.getLogger()
225 _log.info("info")
226 self.assertEqual(cm.records[0].name, "root")
227 self.assertEqual(cm.records[0].levelname, "INFO")
228 all = "\n".join(cm.output)
229 self.assertNotIn("Took", all)
230 self.assertNotIn("memory", all)
232 # Report memory usage including child processes.
233 with self.assertLogs(level="DEBUG") as cm:
234 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_child=True):
235 pass
236 self.assertEqual(cm.records[0].name, "root")
237 self.assertEqual(cm.records[0].levelname, "DEBUG")
238 self.assertIn("Took", cm.output[0])
239 self.assertIn("memory", cm.output[0])
240 self.assertIn("delta", cm.output[0])
241 self.assertIn("peak delta", cm.output[0])
242 self.assertIn("byte", cm.output[0])
244 # Report memory usage, use non-default, but a valid memory unit.
245 with self.assertLogs(level="DEBUG") as cm:
246 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.kilobyte):
247 pass
248 self.assertEqual(cm.records[0].name, "root")
249 self.assertEqual(cm.records[0].levelname, "DEBUG")
250 self.assertIn("Took", cm.output[0])
251 self.assertIn("memory", cm.output[0])
252 self.assertIn("delta", cm.output[0])
253 self.assertIn("peak delta", cm.output[0])
254 self.assertIn("kbyte", cm.output[0])
256 # Report memory usage, use an invalid memory unit.
257 with self.assertLogs(level="DEBUG") as cm:
258 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.gram):
259 pass
260 self.assertEqual(cm.records[0].name, "lsst.utils.timer")
261 self.assertEqual(cm.records[0].levelname, "WARNING")
262 self.assertIn("Invalid", cm.output[0])
263 self.assertIn("byte", cm.output[0])
264 self.assertEqual(cm.records[1].name, "root")
265 self.assertEqual(cm.records[1].levelname, "DEBUG")
266 self.assertIn("Took", cm.output[1])
267 self.assertIn("memory", cm.output[1])
268 self.assertIn("delta", cm.output[1])
269 self.assertIn("peak delta", cm.output[1])
270 self.assertIn("byte", cm.output[1])
272 # Change logging level
273 with self.assertLogs(level="INFO") as cm:
274 with time_this(level=logging.INFO, prefix=None):
275 pass
276 self.assertEqual(cm.records[0].name, "root")
277 self.assertIn("Took", cm.output[0])
278 self.assertIn("seconds", cm.output[0])
280 # Use a new logger with a message.
281 msg = "Test message %d"
282 test_num = 42
283 logname = "test"
284 with self.assertLogs(level="DEBUG") as cm:
285 with time_this(log=logging.getLogger(logname), msg=msg, args=(42,), prefix=None):
286 pass
287 self.assertEqual(cm.records[0].name, logname)
288 self.assertIn("Took", cm.output[0])
289 self.assertIn(msg % test_num, cm.output[0])
291 # Prefix the logger.
292 prefix = "prefix"
293 with self.assertLogs(level="DEBUG") as cm:
294 with time_this(prefix=prefix):
295 pass
296 self.assertEqual(cm.records[0].name, prefix)
297 self.assertIn("Took", cm.output[0])
299 # Prefix explicit logger.
300 with self.assertLogs(level="DEBUG") as cm:
301 with time_this(log=logging.getLogger(logname), prefix=prefix):
302 pass
303 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
305 # Trigger a problem.
306 with self.assertLogs(level="ERROR") as cm:
307 with self.assertRaises(RuntimeError):
308 with time_this(log=logging.getLogger(logname), prefix=prefix):
309 raise RuntimeError("A problem")
310 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
311 self.assertEqual(cm.records[0].levelname, "ERROR")
314class ProfileTestCase(unittest.TestCase):
315 """Test profiling decorator."""
317 def test_profile(self):
318 logger = logging.getLogger("profile")
320 with profile(None) as prof:
321 pass
322 self.assertIsNone(prof)
324 with tempfile.NamedTemporaryFile() as tmp:
325 with self.assertLogs("profile", level=logging.INFO) as cm:
326 with profile(tmp.name, logger) as prof:
327 pass
328 self.assertEqual(len(cm.output), 2)
329 self.assertIsNotNone(prof)
330 self.assertTrue(os.path.exists(tmp.name))
331 self.assertIsInstance(pstats.Stats(tmp.name), pstats.Stats)
334if __name__ == "__main__":
335 unittest.main()