Coverage for tests/test_timer.py: 14%

195 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 21:58 -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. 

11 

12import datetime 

13import logging 

14import os.path 

15import time 

16import unittest 

17from dataclasses import dataclass 

18 

19from astropy import units as u 

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

21 

22log = logging.getLogger("test_timer") 

23 

24THIS_FILE = os.path.basename(__file__) 

25 

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

27# with the function up front. 

28test_metadata = {} 

29 

30 

31@dataclass 

32class Example1: 

33 log: logging.Logger 

34 metadata: dict 

35 

36 @timeMethod 

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

38 """Sleep for some time.""" 

39 time.sleep(duration) 

40 

41 

42@timeMethod 

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

44 time.sleep(duration) 

45 

46 

47@timeMethod(logger=log) 

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

49 time.sleep(duration) 

50 

51 

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

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

54 time.sleep(duration) 

55 

56 

57@timeMethod(metadata=test_metadata) 

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

59 time.sleep(duration) 

60 

61 

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

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 self.assertIn("PrefixMaxResidentSetSize", metadata) 

95 self.assertEqual(metadata["__version__"], 1) 

96 

97 # Again with no log output. 

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

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

100 

101 # With an explicit stacklevel. 

102 with self.assertLogs(level=logging.INFO) as cm: 

103 logInfo( 

104 None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO, stacklevel=0 

105 ) 

106 self.assertEqual(cm.records[0].filename, "timer.py") 

107 

108 def assertTimer(self, duration, task): 

109 # Call it twice to test the "add" functionality. 

110 task.sleeper(duration) 

111 task.sleeper(duration) 

112 counter = 2 

113 

114 has_logger = getattr(task, "log", None) is not None and task.log is not None 

115 has_metadata = getattr(task, "metadata", None) is not None and task.metadata is not None 

116 

117 if has_logger: 

118 counter += 1 

119 with self.assertLogs("timer.task", level=logging.DEBUG) as cm: 

120 task.sleeper(duration) 

121 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here") 

122 

123 if has_metadata: 

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

125 

126 start = datetime.datetime.fromisoformat(task.metadata["sleeperStartUtc"][1]) 

127 end = datetime.datetime.fromisoformat(task.metadata["sleeperEndUtc"][1]) 

128 delta = end - start 

129 delta_sec = delta.seconds + (delta.microseconds / 1e6) 

130 self.assertGreaterEqual(delta_sec, duration) 

131 

132 def testTaskLike(self): 

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

134 

135 # Call with different parameters. 

136 parameters = ( 

137 (logging.getLogger("task"), {}), 

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

139 (None, {}), 

140 (None, None), 

141 ) 

142 

143 duration = 0.1 

144 for log, metadata in parameters: 

145 with self.subTest(log=log, metadata=metadata): 

146 task = Example1(log=log, metadata=metadata) 

147 self.assertTimer(duration, task) 

148 

149 def testDecorated(self): 

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

151 duration = 0.1 

152 

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

154 # and not crash. 

155 decorated_sleeper_nothing(self, duration) 

156 

157 # Use a function decorated for logging. 

158 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm: 

159 decorated_sleeper_logger(self, duration) 

160 self.assertEqual(cm.records[0].filename, THIS_FILE, "log message should originate from here") 

161 

162 # And adjust the log level 

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

164 decorated_sleeper_logger_level(self, duration) 

165 

166 # Use a function decorated for metadata. 

167 self.assertEqual(len(test_metadata), 0) 

168 with self.assertLogs("timer.test_timer", level=logging.DEBUG) as cm: 

169 # Check that we only get a single log message and nothing from 

170 # timeMethod itself. 

171 decorated_sleeper_metadata(self, duration) 

172 logging.getLogger("timer.test_timer").debug("sentinel") 

173 self.assertEqual(len(cm.output), 1) 

174 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata) 

175 

176 

177class TimerTestCase(unittest.TestCase): 

178 def testTimer(self): 

179 with self.assertLogs(level="DEBUG") as cm: 

180 with time_this(): 

181 pass 

182 self.assertEqual(cm.records[0].name, "timer") 

183 self.assertEqual(cm.records[0].levelname, "DEBUG") 

184 self.assertEqual(cm.records[0].filename, THIS_FILE) 

185 

186 with self.assertLogs(level="DEBUG") as cm: 

187 with time_this(prefix=None): 

188 pass 

189 self.assertEqual(cm.records[0].name, "root") 

190 self.assertEqual(cm.records[0].levelname, "DEBUG") 

191 self.assertIn("Took", cm.output[0]) 

192 self.assertNotIn(": Took", cm.output[0]) 

193 self.assertNotIn("; ", cm.output[0]) 

194 self.assertEqual(cm.records[0].filename, THIS_FILE) 

195 

196 # Report memory usage. 

197 with self.assertLogs(level="DEBUG") as cm: 

198 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True): 

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.assertIn("memory", cm.output[0]) 

204 self.assertIn("delta", cm.output[0]) 

205 self.assertIn("peak delta", cm.output[0]) 

206 self.assertIn("byte", cm.output[0]) 

207 

208 # Report memory usage including child processes. 

209 with self.assertLogs(level="DEBUG") as cm: 

210 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_child=True): 

211 pass 

212 self.assertEqual(cm.records[0].name, "root") 

213 self.assertEqual(cm.records[0].levelname, "DEBUG") 

214 self.assertIn("Took", cm.output[0]) 

215 self.assertIn("memory", cm.output[0]) 

216 self.assertIn("delta", cm.output[0]) 

217 self.assertIn("peak delta", cm.output[0]) 

218 self.assertIn("byte", cm.output[0]) 

219 

220 # Report memory usage, use non-default, but a valid memory unit. 

221 with self.assertLogs(level="DEBUG") as cm: 

222 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.kilobyte): 

223 pass 

224 self.assertEqual(cm.records[0].name, "root") 

225 self.assertEqual(cm.records[0].levelname, "DEBUG") 

226 self.assertIn("Took", cm.output[0]) 

227 self.assertIn("memory", cm.output[0]) 

228 self.assertIn("delta", cm.output[0]) 

229 self.assertIn("peak delta", cm.output[0]) 

230 self.assertIn("kbyte", cm.output[0]) 

231 

232 # Report memory usage, use an invalid memory unit. 

233 with self.assertLogs(level="DEBUG") as cm: 

234 with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_unit=u.gram): 

235 pass 

236 self.assertEqual(cm.records[0].name, "lsst.utils.timer") 

237 self.assertEqual(cm.records[0].levelname, "WARNING") 

238 self.assertIn("Invalid", cm.output[0]) 

239 self.assertIn("byte", cm.output[0]) 

240 self.assertEqual(cm.records[1].name, "root") 

241 self.assertEqual(cm.records[1].levelname, "DEBUG") 

242 self.assertIn("Took", cm.output[1]) 

243 self.assertIn("memory", cm.output[1]) 

244 self.assertIn("delta", cm.output[1]) 

245 self.assertIn("peak delta", cm.output[1]) 

246 self.assertIn("byte", cm.output[1]) 

247 

248 # Change logging level 

249 with self.assertLogs(level="INFO") as cm: 

250 with time_this(level=logging.INFO, prefix=None): 

251 pass 

252 self.assertEqual(cm.records[0].name, "root") 

253 self.assertIn("Took", cm.output[0]) 

254 self.assertIn("seconds", cm.output[0]) 

255 

256 # Use a new logger with a message. 

257 msg = "Test message %d" 

258 test_num = 42 

259 logname = "test" 

260 with self.assertLogs(level="DEBUG") as cm: 

261 with time_this(log=logging.getLogger(logname), msg=msg, args=(42,), prefix=None): 

262 pass 

263 self.assertEqual(cm.records[0].name, logname) 

264 self.assertIn("Took", cm.output[0]) 

265 self.assertIn(msg % test_num, cm.output[0]) 

266 

267 # Prefix the logger. 

268 prefix = "prefix" 

269 with self.assertLogs(level="DEBUG") as cm: 

270 with time_this(prefix=prefix): 

271 pass 

272 self.assertEqual(cm.records[0].name, prefix) 

273 self.assertIn("Took", cm.output[0]) 

274 

275 # Prefix explicit logger. 

276 with self.assertLogs(level="DEBUG") as cm: 

277 with time_this(log=logging.getLogger(logname), prefix=prefix): 

278 pass 

279 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}") 

280 

281 # Trigger a problem. 

282 with self.assertLogs(level="ERROR") as cm: 

283 with self.assertRaises(RuntimeError): 

284 with time_this(log=logging.getLogger(logname), prefix=prefix): 

285 raise RuntimeError("A problem") 

286 self.assertEqual(cm.records[0].name, f"{prefix}.{logname}") 

287 self.assertEqual(cm.records[0].levelname, "ERROR") 

288 

289 

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

291 unittest.main()