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

146 statements  

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. 

11 

12import unittest 

13import logging 

14import time 

15import datetime 

16import os.path 

17from dataclasses import dataclass 

18 

19from lsst.utils.timer import timeMethod, logPairs, time_this, logInfo 

20 

21log = logging.getLogger("test_timer") 

22 

23THIS_FILE = os.path.basename(__file__) 

24 

25# Only use this in a single test but needs to be associated 

26# with the function up front. 

27test_metadata = {} 

28 

29 

30@dataclass 

31class Example1: 

32 log: logging.Logger 

33 metadata: dict 

34 

35 @timeMethod 

36 def sleeper(self, duration: float) -> None: 

37 """Sleep for some time.""" 

38 time.sleep(duration) 

39 

40 

41@timeMethod 

42def decorated_sleeper_nothing(self, duration: float) -> None: 

43 time.sleep(duration) 

44 

45 

46@timeMethod(logger=log) 

47def decorated_sleeper_logger(self, duration: float) -> None: 

48 time.sleep(duration) 

49 

50 

51@timeMethod(logger=log, logLevel=logging.INFO) 

52def decorated_sleeper_logger_level(self, duration: float) -> None: 

53 time.sleep(duration) 

54 

55 

56@timeMethod(metadata=test_metadata) 

57def decorated_sleeper_metadata(self, duration: float) -> None: 

58 time.sleep(duration) 

59 

60 

61class TestTimeMethod(unittest.TestCase): 

62 

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]}) 

74 

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") 

80 

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) 

86 

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) 

94 

95 # Again with no log output. 

96 logInfo(None, prefix="Prefix", metadata=metadata) 

97 self.assertEqual(len(metadata["PrefixUtc"]), 2) 

98 

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") 

104 

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 

110 

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 

115 

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") 

121 

122 if has_metadata: 

123 self.assertEqual(len(task.metadata["sleeperStartUserTime"]), counter) 

124 

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) 

130 

131 def testTaskLike(self): 

132 """Test timer on something that looks like a Task.""" 

133 

134 # Call with different parameters. 

135 parameters = ((logging.getLogger("task"), {}), 

136 (logging.getLogger("task"), None), 

137 (None, {}), 

138 (None, None)) 

139 

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) 

145 

146 def testDecorated(self): 

147 """Test timeMethod on non-Task like instances.""" 

148 duration = 0.1 

149 

150 # The "self" object shouldn't be usable but this should do nothing 

151 # and not crash. 

152 decorated_sleeper_nothing(self, duration) 

153 

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") 

158 

159 # And adjust the log level 

160 with self.assertLogs("timer.test_timer", level=logging.INFO): 

161 decorated_sleeper_logger_level(self, duration) 

162 

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) 

172 

173 

174class TimerTestCase(unittest.TestCase): 

175 

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) 

183 

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) 

191 

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]) 

199 

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]) 

211 

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]) 

219 

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}") 

226 

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") 

235 

236 

237if __name__ == "__main__": 237 ↛ 238line 237 didn't jump to line 238, because the condition on line 237 was never true

238 unittest.main()