Coverage for tests/test_timer.py: 13%

219 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-01 02:29 -0700

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 pstats 

16import tempfile 

17import time 

18import unittest 

19from dataclasses import dataclass 

20 

21from astropy import units as u 

22from lsst.utils.timer import logInfo, logPairs, profile, time_this, timeMethod 

23 

24log = logging.getLogger("test_timer") 

25 

26THIS_FILE = os.path.basename(__file__) 

27 

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

29# with the function up front. 

30test_metadata = {} 

31 

32 

33@dataclass 

34class Example1: 

35 log: logging.Logger 

36 metadata: dict 

37 

38 @timeMethod 

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

40 """Sleep for some time.""" 

41 time.sleep(duration) 

42 

43 

44@timeMethod 

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

46 time.sleep(duration) 

47 

48 

49@timeMethod(logger=log) 

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

51 time.sleep(duration) 

52 

53 

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

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

56 time.sleep(duration) 

57 

58 

59@timeMethod(metadata=test_metadata) 

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

61 time.sleep(duration) 

62 

63 

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

76 

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

82 

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) 

88 

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) 

98 

99 # Again with no log output. 

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

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

102 

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

109 

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 

115 

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 

118 

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

124 

125 if has_metadata: 

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

127 

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) 

133 

134 def testTaskLike(self): 

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

136 

137 # Call with different parameters. 

138 parameters = ( 

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

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

141 (None, {}), 

142 (None, None), 

143 ) 

144 

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) 

150 

151 def testDecorated(self): 

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

153 duration = 0.1 

154 

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

156 # and not crash. 

157 decorated_sleeper_nothing(self, duration) 

158 

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

163 

164 # And adjust the log level 

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

166 decorated_sleeper_logger_level(self, duration) 

167 

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) 

177 

178 

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) 

187 

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) 

197 

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

209 

210 # Request memory usage but with log level that will not issue it. 

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

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

213 pass 

214 # Ensure that a log message is issued. 

215 _log = logging.getLogger() 

216 _log.info("info") 

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

218 self.assertEqual(cm.records[0].levelname, "INFO") 

219 all = "\n".join(cm.output) 

220 self.assertNotIn("Took", all) 

221 self.assertNotIn("memory", all) 

222 

223 # Report memory usage including child processes. 

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

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

226 pass 

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

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

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

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

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

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

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

234 

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

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

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

238 pass 

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

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

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

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

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

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

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

246 

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

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

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

250 pass 

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

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

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

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

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

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

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

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

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

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

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

262 

263 # Change logging level 

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

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

266 pass 

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

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

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

270 

271 # Use a new logger with a message. 

272 msg = "Test message %d" 

273 test_num = 42 

274 logname = "test" 

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

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

277 pass 

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

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

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

281 

282 # Prefix the logger. 

283 prefix = "prefix" 

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

285 with time_this(prefix=prefix): 

286 pass 

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

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

289 

290 # Prefix explicit logger. 

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

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

293 pass 

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

295 

296 # Trigger a problem. 

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

298 with self.assertRaises(RuntimeError): 

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

300 raise RuntimeError("A problem") 

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

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

303 

304 

305class ProfileTestCase(unittest.TestCase): 

306 def test_profile(self): 

307 logger = logging.getLogger("profile") 

308 

309 with profile(None) as prof: 

310 pass 

311 self.assertIsNone(prof) 

312 

313 with tempfile.NamedTemporaryFile() as tmp: 

314 with self.assertLogs("profile", level=logging.INFO) as cm: 

315 with profile(tmp.name, logger) as prof: 

316 pass 

317 self.assertEqual(len(cm.output), 2) 

318 self.assertIsNotNone(prof) 

319 self.assertTrue(os.path.exists(tmp.name)) 

320 self.assertIsInstance(pstats.Stats(tmp.name), pstats.Stats), 

321 

322 

323if __name__ == "__main__": 

324 unittest.main()