Coverage for tests/test_timer.py: 14%
211 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-09 02:45 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-09 02:45 -0800
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 # Report memory usage including child processes.
211 with self.assertLogs(level="DEBUG") as cm:
212 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_child=True):
213 pass
214 self.assertEqual(cm.records[0].name, "root")
215 self.assertEqual(cm.records[0].levelname, "DEBUG")
216 self.assertIn("Took", cm.output[0])
217 self.assertIn("memory", cm.output[0])
218 self.assertIn("delta", cm.output[0])
219 self.assertIn("peak delta", cm.output[0])
220 self.assertIn("byte", cm.output[0])
222 # Report memory usage, use non-default, but a valid memory unit.
223 with self.assertLogs(level="DEBUG") as cm:
224 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.kilobyte):
225 pass
226 self.assertEqual(cm.records[0].name, "root")
227 self.assertEqual(cm.records[0].levelname, "DEBUG")
228 self.assertIn("Took", cm.output[0])
229 self.assertIn("memory", cm.output[0])
230 self.assertIn("delta", cm.output[0])
231 self.assertIn("peak delta", cm.output[0])
232 self.assertIn("kbyte", cm.output[0])
234 # Report memory usage, use an invalid memory unit.
235 with self.assertLogs(level="DEBUG") as cm:
236 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.gram):
237 pass
238 self.assertEqual(cm.records[0].name, "lsst.utils.timer")
239 self.assertEqual(cm.records[0].levelname, "WARNING")
240 self.assertIn("Invalid", cm.output[0])
241 self.assertIn("byte", cm.output[0])
242 self.assertEqual(cm.records[1].name, "root")
243 self.assertEqual(cm.records[1].levelname, "DEBUG")
244 self.assertIn("Took", cm.output[1])
245 self.assertIn("memory", cm.output[1])
246 self.assertIn("delta", cm.output[1])
247 self.assertIn("peak delta", cm.output[1])
248 self.assertIn("byte", cm.output[1])
250 # Change logging level
251 with self.assertLogs(level="INFO") as cm:
252 with time_this(level=logging.INFO, prefix=None):
253 pass
254 self.assertEqual(cm.records[0].name, "root")
255 self.assertIn("Took", cm.output[0])
256 self.assertIn("seconds", cm.output[0])
258 # Use a new logger with a message.
259 msg = "Test message %d"
260 test_num = 42
261 logname = "test"
262 with self.assertLogs(level="DEBUG") as cm:
263 with time_this(log=logging.getLogger(logname), msg=msg, args=(42,), prefix=None):
264 pass
265 self.assertEqual(cm.records[0].name, logname)
266 self.assertIn("Took", cm.output[0])
267 self.assertIn(msg % test_num, cm.output[0])
269 # Prefix the logger.
270 prefix = "prefix"
271 with self.assertLogs(level="DEBUG") as cm:
272 with time_this(prefix=prefix):
273 pass
274 self.assertEqual(cm.records[0].name, prefix)
275 self.assertIn("Took", cm.output[0])
277 # Prefix explicit logger.
278 with self.assertLogs(level="DEBUG") as cm:
279 with time_this(log=logging.getLogger(logname), prefix=prefix):
280 pass
281 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
283 # Trigger a problem.
284 with self.assertLogs(level="ERROR") as cm:
285 with self.assertRaises(RuntimeError):
286 with time_this(log=logging.getLogger(logname), prefix=prefix):
287 raise RuntimeError("A problem")
288 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
289 self.assertEqual(cm.records[0].levelname, "ERROR")
292class ProfileTestCase(unittest.TestCase):
293 def test_profile(self):
294 logger = logging.getLogger("profile")
296 with profile(None) as prof:
297 pass
298 self.assertIsNone(prof)
300 with tempfile.NamedTemporaryFile() as tmp:
301 with self.assertLogs("profile", level=logging.INFO) as cm:
302 with profile(tmp.name, logger) as prof:
303 pass
304 self.assertEqual(len(cm.output), 2)
305 self.assertIsNotNone(prof)
306 self.assertTrue(os.path.exists(tmp.name))
307 self.assertIsInstance(pstats.Stats(tmp.name), pstats.Stats),
310if __name__ == "__main__": 310 ↛ 311line 310 didn't jump to line 311, because the condition on line 310 was never true
311 unittest.main()