Coverage for tests/test_timer.py: 13%

219 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-08 09:53 +0000

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 """Test class with log and metadata properties, similar to ``Task``.""" 

36 

37 log: logging.Logger 

38 metadata: dict 

39 

40 @timeMethod 

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

42 """Sleep for some time.""" 

43 time.sleep(duration) 

44 

45 

46@timeMethod 

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

48 """Test function that sleeps.""" 

49 time.sleep(duration) 

50 

51 

52@timeMethod(logger=log) 

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

54 """Test function that sleeps and logs.""" 

55 time.sleep(duration) 

56 

57 

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

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

60 """Test function that logs at INFO.""" 

61 time.sleep(duration) 

62 

63 

64@timeMethod(metadata=test_metadata) 

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

66 """Test function that uses external metadata.""" 

67 time.sleep(duration) 

68 

69 

70class TestTimeMethod(unittest.TestCase): 

71 """Test the time method decorator.""" 

72 

73 def testLogPairs(self): 

74 # Test the non-obj case. 

75 logger = logging.getLogger("test") 

76 pairs = (("name1", 0), ("name2", 1)) 

77 metadata = {} 

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

79 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata) 

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

81 self.assertTrue(cm.output[0].endswith("name1=0; name2=1"), cm.output) 

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

83 self.assertEqual(metadata, {"name1": [0], "name2": [1]}) 

84 

85 # Call it again with an explicit stack level. 

86 # Force it to come from lsst.utils. 

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

88 logPairs(None, pairs, logLevel=logging.INFO, logger=logger, metadata=metadata, stacklevel=0) 

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

90 

91 # Check that the log message is filtered by default. 

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

93 logPairs(None, pairs, logger=logger, metadata=metadata) 

94 logger.info("Message") 

95 self.assertEqual(len(cm.records), 1) 

96 

97 def testLogInfo(self): 

98 metadata = {} 

99 logger = logging.getLogger("testLogInfo") 

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

101 logInfo(None, prefix="Prefix", metadata=metadata, logger=logger, logLevel=logging.INFO) 

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

103 self.assertIn("PrefixUtc", metadata) 

104 self.assertIn("PrefixMaxResidentSetSize", metadata) 

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

106 

107 # Again with no log output. 

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

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

110 

111 # With an explicit stacklevel. 

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

113 logInfo( 

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

115 ) 

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

117 

118 def assertTimer(self, duration, task): 

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

120 task.sleeper(duration) 

121 task.sleeper(duration) 

122 counter = 2 

123 

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

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

126 

127 if has_logger: 

128 counter += 1 

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

130 task.sleeper(duration) 

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

132 

133 if has_metadata: 

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

135 

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

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

138 delta = end - start 

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

140 self.assertGreaterEqual(delta_sec, duration) 

141 

142 def testTaskLike(self): 

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

144 # Call with different parameters. 

145 parameters = ( 

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

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

148 (None, {}), 

149 (None, None), 

150 ) 

151 

152 duration = 0.1 

153 for log, metadata in parameters: 

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

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

156 self.assertTimer(duration, task) 

157 

158 def testDecorated(self): 

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

160 duration = 0.1 

161 

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

163 # and not crash. 

164 decorated_sleeper_nothing(self, duration) 

165 

166 # Use a function decorated for logging. 

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

168 decorated_sleeper_logger(self, duration) 

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

170 

171 # And adjust the log level 

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

173 decorated_sleeper_logger_level(self, duration) 

174 

175 # Use a function decorated for metadata. 

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

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

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

179 # timeMethod itself. 

180 decorated_sleeper_metadata(self, duration) 

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

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

183 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata) 

184 

185 

186class TimerTestCase(unittest.TestCase): 

187 """Test the timer functionality.""" 

188 

189 def testTimer(self): 

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

191 with time_this(): 

192 pass 

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

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

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

196 

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

198 with time_this(prefix=None): 

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.assertNotIn(": Took", cm.output[0]) 

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

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

206 

207 # Report memory usage. 

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

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

210 pass 

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

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

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

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

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

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

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

218 

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

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

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

222 pass 

223 # Ensure that a log message is issued. 

224 _log = logging.getLogger() 

225 _log.info("info") 

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

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

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

229 self.assertNotIn("Took", all) 

230 self.assertNotIn("memory", all) 

231 

232 # Report memory usage including child processes. 

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

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

235 pass 

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

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

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

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

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

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

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

243 

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

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

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

247 pass 

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

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

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

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

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

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

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

255 

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

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

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

259 pass 

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

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

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

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

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

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

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

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

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

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

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

271 

272 # Change logging level 

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

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

275 pass 

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

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

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

279 

280 # Use a new logger with a message. 

281 msg = "Test message %d" 

282 test_num = 42 

283 logname = "test" 

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

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

286 pass 

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

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

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

290 

291 # Prefix the logger. 

292 prefix = "prefix" 

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

294 with time_this(prefix=prefix): 

295 pass 

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

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

298 

299 # Prefix explicit logger. 

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

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

302 pass 

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

304 

305 # Trigger a problem. 

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

307 with self.assertRaises(RuntimeError): 

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

309 raise RuntimeError("A problem") 

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

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

312 

313 

314class ProfileTestCase(unittest.TestCase): 

315 """Test profiling decorator.""" 

316 

317 def test_profile(self): 

318 logger = logging.getLogger("profile") 

319 

320 with profile(None) as prof: 

321 pass 

322 self.assertIsNone(prof) 

323 

324 with tempfile.NamedTemporaryFile() as tmp: 

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

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

327 pass 

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

329 self.assertIsNotNone(prof) 

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

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

332 

333 

334if __name__ == "__main__": 

335 unittest.main()