Coverage for tests/test_timer.py: 25%
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 datetime
13import logging
14import os.path
15import time
16import unittest
17from dataclasses import dataclass
19from lsst.utils.timer import logInfo, logPairs, time_this, timeMethod
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):
62 def testLogPairs(self):
63 # Test the non-obj case.
64 logger = logging.getLogger("test")
65 pairs = (("name1", 0), ("name2", 1))
66 metadata = {}
67 with self.assertLogs(level=logging.INFO) as cm:
68 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata)
69 self.assertEqual(len(cm.output), 1, cm.output)
70 self.assertTrue(cm.output[0].endswith("name1=0; name2=1"), cm.output)
71 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
72 self.assertEqual(metadata, {"name1": [0], "name2": [1]})
74 # Call it again with an explicit stack level.
75 # Force it to come from lsst.utils.
76 with self.assertLogs(level=logging.INFO) as cm:
77 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata, stacklevel=0)
78 self.assertEqual(cm.records[0].filename, "timer.py")
80 # Check that the log message is filtered by default.
81 with self.assertLogs(level=logging.INFO) as cm:
82 logPairs(None, pairs, logger=logger, metadata=metadata)
83 logger.info("Message")
84 self.assertEqual(len(cm.records), 1)
86 def testLogInfo(self):
87 metadata = {}
88 logger = logging.getLogger("testLogInfo")
89 with self.assertLogs(level=logging.INFO) as cm:
90 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO)
91 self.assertEqual(cm.records[0].filename, THIS_FILE)
92 self.assertIn("PrefixUtc", metadata)
94 # Again with no log output.
95 logInfo(None, prefix="Prefix", metadata=metadata)
96 self.assertEqual(len(metadata["PrefixUtc"]), 2)
98 # With an explicit stacklevel.
99 with self.assertLogs(level=logging.INFO) as cm:
100 logInfo(
101 None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO, stacklevel=0
102 )
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 and task.log is not None
112 has_metadata = getattr(task, "metadata", None) is not None and task.metadata is not None
114 if has_logger:
115 counter += 1
116 with self.assertLogs("timer.task", level=logging.DEBUG) as cm:
117 task.sleeper(duration)
118 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here")
120 if has_metadata:
121 self.assertEqual(len(task.metadata["sleeperStartUserTime"]), counter)
123 start = datetime.datetime.fromisoformat(task.metadata["sleeperStartUtc"][1])
124 end = datetime.datetime.fromisoformat(task.metadata["sleeperEndUtc"][1])
125 delta = end - start
126 delta_sec = delta.seconds + (delta.microseconds / 1e6)
127 self.assertGreaterEqual(delta_sec, duration)
129 def testTaskLike(self):
130 """Test timer on something that looks like a Task."""
132 # Call with different parameters.
133 parameters = (
134 (logging.getLogger("task"), {}),
135 (logging.getLogger("task"), None),
136 (None, {}),
137 (None, None),
138 )
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):
175 def testTimer(self):
176 with self.assertLogs(level="DEBUG") as cm:
177 with time_this():
178 pass
179 self.assertEqual(cm.records[0].name, "timer")
180 self.assertEqual(cm.records[0].levelname, "DEBUG")
181 self.assertEqual(cm.records[0].filename, THIS_FILE)
183 with self.assertLogs(level="DEBUG") as cm:
184 with time_this(prefix=None):
185 pass
186 self.assertEqual(cm.records[0].name, "root")
187 self.assertEqual(cm.records[0].levelname, "DEBUG")
188 self.assertIn("Took", cm.output[0])
189 self.assertEqual(cm.records[0].filename, THIS_FILE)
191 # Change logging level
192 with self.assertLogs(level="INFO") as cm:
193 with time_this(level=logging.INFO, prefix=None):
194 pass
195 self.assertEqual(cm.records[0].name, "root")
196 self.assertIn("Took", cm.output[0])
197 self.assertIn("seconds", cm.output[0])
199 # Use a new logger with a message.
200 msg = "Test message %d"
201 test_num = 42
202 logname = "test"
203 with self.assertLogs(level="DEBUG") as cm:
204 with time_this(log=logging.getLogger(logname), msg=msg, args=(42,), prefix=None):
205 pass
206 self.assertEqual(cm.records[0].name, logname)
207 self.assertIn("Took", cm.output[0])
208 self.assertIn(msg % test_num, cm.output[0])
210 # Prefix the logger.
211 prefix = "prefix"
212 with self.assertLogs(level="DEBUG") as cm:
213 with time_this(prefix=prefix):
214 pass
215 self.assertEqual(cm.records[0].name, prefix)
216 self.assertIn("Took", cm.output[0])
218 # Prefix explicit logger.
219 with self.assertLogs(level="DEBUG") as cm:
220 with time_this(log=logging.getLogger(logname), prefix=prefix):
221 pass
222 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
224 # Trigger a problem.
225 with self.assertLogs(level="ERROR") as cm:
226 with self.assertRaises(RuntimeError):
227 with time_this(log=logging.getLogger(logname), prefix=prefix):
228 raise RuntimeError("A problem")
229 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}")
230 self.assertEqual(cm.records[0].levelname, "ERROR")
233if __name__ == "__main__": 233 ↛ 234line 233 didn't jump to line 234, because the condition on line 233 was never true
234 unittest.main()