Coverage for tests/test_timer.py: 23%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 unittest
13import logging
14import time
15import datetime
16import os.path
17from dataclasses import dataclass
19from lsst.utils.timer import timeMethod, logPairs, time_this, logInfo
21log = logging.getLogger("test_timer")
23THIS_FILE = os.path.basename(__file__)
25# Only use this in a single test but needs to be associated
26# with the function up front.
27test_metadata = {}
30@dataclass
31class Example1:
32 log: logging.Logger
33 metadata: dict
35 @timeMethod
36 def sleeper(self, duration: float) -> None:
37 """Sleep for some time."""
38 time.sleep(duration)
41@timeMethod
42def decorated_sleeper_nothing(self, duration: float) -> None:
43 time.sleep(duration)
46@timeMethod(logger=log)
47def decorated_sleeper_logger(self, duration: float) -> None:
48 time.sleep(duration)
51@timeMethod(logger=log, logLevel=logging.INFO)
52def decorated_sleeper_logger_level(self, duration: float) -> None:
53 time.sleep(duration)
56@timeMethod(metadata=test_metadata)
57def decorated_sleeper_metadata(self, duration: float) -> None:
58 time.sleep(duration)
61class TestTimeMethod(unittest.TestCase):
63 def testLogPairs(self):
64 # Test the non-obj case.
65 logger = logging.getLogger("test")
66 pairs = (("name1", 0), ("name2", 1))
67 metadata = {}
68 with self.assertLogs(level=logging.INFO) as cm:
69 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata)
70 self.assertEqual(len(cm.output), 1, cm.output)
71 self.assertTrue(cm.output[0].endswith("name1=0; name2=1"), cm.output)
72 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
73 self.assertEqual(metadata, {"name1": [0], "name2": [1]})
75 # Call it again with an explicit stack level.
76 # Force it to come from lsst.utils.
77 with self.assertLogs(level=logging.INFO) as cm:
78 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata, stacklevel=0)
79 self.assertEqual(cm.records[0].filename, "timer.py")
81 # Check that the log message is filtered by default.
82 with self.assertLogs(level=logging.INFO) as cm:
83 logPairs(None, pairs, logger=logger, metadata=metadata)
84 logger.info("Message")
85 self.assertEqual(len(cm.records), 1)
87 def testLogInfo(self):
88 metadata = {}
89 logger = logging.getLogger("testLogInfo")
90 with self.assertLogs(level=logging.INFO) as cm:
91 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO)
92 self.assertEqual(cm.records[0].filename, THIS_FILE)
93 self.assertIn("PrefixUtc", metadata)
95 # Again with no log output.
96 logInfo(None, prefix="Prefix", metadata=metadata)
97 self.assertEqual(len(metadata["PrefixUtc"]), 2)
99 # With an explicit stacklevel.
100 with self.assertLogs(level=logging.INFO) as cm:
101 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO,
102 stacklevel=0)
103 self.assertEqual(cm.records[0].filename, "timer.py")
105 def assertTimer(self, duration, task):
106 # Call it twice to test the "add" functionality.
107 task.sleeper(duration)
108 task.sleeper(duration)
109 counter = 2
111 has_logger = getattr(task, "log", None) is not None \
112 and task.log is not None
113 has_metadata = getattr(task, "metadata", None) is not None \
114 and task.metadata is not None
116 if has_logger:
117 counter += 1
118 with self.assertLogs("timer.task", level=logging.DEBUG) as cm:
119 task.sleeper(duration)
120 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
122 if has_metadata:
123 self.assertEqual(len(task.metadata["sleeperStartUserTime"]), counter)
125 start = datetime.datetime.fromisoformat(task.metadata["sleeperStartUtc"][1])
126 end = datetime.datetime.fromisoformat(task.metadata["sleeperEndUtc"][1])
127 delta = end - start
128 delta_sec = delta.seconds + (delta.microseconds / 1e6)
129 self.assertGreaterEqual(delta_sec, duration)
131 def testTaskLike(self):
132 """Test timer on something that looks like a Task."""
134 # Call with different parameters.
135 parameters = ((logging.getLogger("task"), {}),
136 (logging.getLogger("task"), None),
137 (None, {}),
138 (None, None))
140 duration = 0.1
141 for log, metadata in parameters:
142 with self.subTest(log=log, metadata=metadata):
143 task = Example1(log=log, metadata=metadata)
144 self.assertTimer(duration, task)
146 def testDecorated(self):
147 """Test timeMethod on non-Task like instances."""
148 duration = 0.1
150 # The "self" object shouldn't be usable but this should do nothing
151 # and not crash.
152 decorated_sleeper_nothing(self, duration)
154 # Use a function decorated for logging.
155 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
156 decorated_sleeper_logger(self, duration)
157 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
159 # And adjust the log level
160 with self.assertLogs("timer.test_timer", level=logging.INFO):
161 decorated_sleeper_logger_level(self, duration)
163 # Use a function decorated for metadata.
164 self.assertEqual(len(test_metadata), 0)
165 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm:
166 # Check that we only get a single log message and nothing from
167 # timeMethod itself.
168 decorated_sleeper_metadata(self, duration)
169 logging.getLogger("timer.test_timer").debug("sentinel")
170 self.assertEqual(len(cm.output), 1)
171 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata)
174class TimerTestCase(unittest.TestCase):
176 def testTimer(self):
177 with self.assertLogs(level="DEBUG") as cm:
178 with time_this():
179 pass
180 self.assertEqual(cm.records[0].name, "timer")
181 self.assertEqual(cm.records[0].levelname, "DEBUG")
182 self.assertEqual(cm.records[0].filename, THIS_FILE)
184 with self.assertLogs(level="DEBUG") as cm:
185 with time_this(prefix=None):
186 pass
187 self.assertEqual(cm.records[0].name, "root")
188 self.assertEqual(cm.records[0].levelname, "DEBUG")
189 self.assertIn("Took", cm.output[0])
190 self.assertEqual(cm.records[0].filename, THIS_FILE)
192 # Change logging level
193 with self.assertLogs(level="INFO") as cm:
194 with time_this(level=logging.INFO, prefix=None):
195 pass
196 self.assertEqual(cm.records[0].name, "root")
197 self.assertIn("Took", cm.output[0])
198 self.assertIn("seconds", cm.output[0])
200 # Use a new logger with a message.
201 msg = "Test message %d"
202 test_num = 42
203 logname = "test"
204 with self.assertLogs(level="DEBUG") as cm:
205 with time_this(log=logging.getLogger(logname),
206 msg=msg, args=(42,), prefix=None):
207 pass
208 self.assertEqual(cm.records[0].name, logname)
209 self.assertIn("Took", cm.output[0])
210 self.assertIn(msg % test_num, cm.output[0])
212 # Prefix the logger.
213 prefix = "prefix"
214 with self.assertLogs(level="DEBUG") as cm:
215 with time_this(prefix=prefix):
216 pass
217 self.assertEqual(cm.records[0].name, prefix)
218 self.assertIn("Took", cm.output[0])
220 # Prefix explicit logger.
221 with self.assertLogs(level="DEBUG") as cm:
222 with time_this(log=logging.getLogger(logname),
223 prefix=prefix):
224 pass
225 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
227 # Trigger a problem.
228 with self.assertLogs(level="ERROR") as cm:
229 with self.assertRaises(RuntimeError):
230 with time_this(log=logging.getLogger(logname),
231 prefix=prefix):
232 raise RuntimeError("A problem")
233 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
234 self.assertEqual(cm.records[0].levelname, "ERROR")
237if __name__ == "__main__": 237 ↛ 238line 237 didn't jump to line 238, because the condition on line 237 was never true
238 unittest.main()