Coverage for tests/test_timer.py: 16%

219 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-17 07: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# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import datetime 

23import logging 

24import os.path 

25import pstats 

26import tempfile 

27import time 

28import unittest 

29from dataclasses import dataclass 

30 

31from astropy import units as u 

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

33 

34log = logging.getLogger("test_timer") 

35 

36THIS_FILE = os.path.basename(__file__) 

37 

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

39# with the function up front. 

40test_metadata = {} 

41 

42 

43@dataclass 

44class Example1: 

45 """Test class with log and metadata properties, similar to ``Task``.""" 

46 

47 log: logging.Logger 

48 metadata: dict 

49 

50 @timeMethod 

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

52 """Sleep for some time.""" 

53 time.sleep(duration) 

54 

55 

56@timeMethod 

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

58 """Test function that sleeps.""" 

59 time.sleep(duration) 

60 

61 

62@timeMethod(logger=log) 

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

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

65 time.sleep(duration) 

66 

67 

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

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

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

71 time.sleep(duration) 

72 

73 

74@timeMethod(metadata=test_metadata) 

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

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

77 time.sleep(duration) 

78 

79 

80class TestTimeMethod(unittest.TestCase): 

81 """Test the time method decorator.""" 

82 

83 def testLogPairs(self): 

84 # Test the non-obj case. 

85 logger = logging.getLogger("test") 

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

87 metadata = {} 

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

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

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

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

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

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

94 

95 # Call it again with an explicit stack level. 

96 # Force it to come from lsst.utils. 

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

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

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

100 

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

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

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

104 logger.info("Message") 

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

106 

107 def testLogInfo(self): 

108 metadata = {} 

109 logger = logging.getLogger("testLogInfo") 

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

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

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

113 self.assertIn("PrefixUtc", metadata) 

114 self.assertIn("PrefixMaxResidentSetSize", metadata) 

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

116 

117 # Again with no log output. 

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

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

120 

121 # With an explicit stacklevel. 

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

123 logInfo( 

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

125 ) 

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

127 

128 def assertTimer(self, duration, task): 

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

130 task.sleeper(duration) 

131 task.sleeper(duration) 

132 counter = 2 

133 

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

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

136 

137 if has_logger: 

138 counter += 1 

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

140 task.sleeper(duration) 

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

142 

143 if has_metadata: 

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

145 

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

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

148 delta = end - start 

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

150 self.assertGreaterEqual(delta_sec, duration) 

151 

152 def testTaskLike(self): 

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

154 # Call with different parameters. 

155 parameters = ( 

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

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

158 (None, {}), 

159 (None, None), 

160 ) 

161 

162 duration = 0.1 

163 for log, metadata in parameters: 

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

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

166 self.assertTimer(duration, task) 

167 

168 def testDecorated(self): 

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

170 duration = 0.1 

171 

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

173 # and not crash. 

174 decorated_sleeper_nothing(self, duration) 

175 

176 # Use a function decorated for logging. 

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

178 decorated_sleeper_logger(self, duration) 

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

180 

181 # And adjust the log level 

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

183 decorated_sleeper_logger_level(self, duration) 

184 

185 # Use a function decorated for metadata. 

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

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

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

189 # timeMethod itself. 

190 decorated_sleeper_metadata(self, duration) 

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

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

193 self.assertIn("decorated_sleeper_metadataStartUserTime", test_metadata) 

194 

195 

196class TimerTestCase(unittest.TestCase): 

197 """Test the timer functionality.""" 

198 

199 def testTimer(self): 

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

201 with time_this(): 

202 pass 

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

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

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

206 

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

208 with time_this(prefix=None): 

209 pass 

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

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

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

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

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

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

216 

217 # Report memory usage. 

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

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

220 pass 

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

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

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

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

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

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

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

228 

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

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

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

232 pass 

233 # Ensure that a log message is issued. 

234 _log = logging.getLogger() 

235 _log.info("info") 

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

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

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

239 self.assertNotIn("Took", all) 

240 self.assertNotIn("memory", all) 

241 

242 # Report memory usage including child processes. 

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

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

245 pass 

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

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

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

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

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

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

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

253 

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

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

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

257 pass 

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

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

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

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

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

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

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

265 

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

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

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

269 pass 

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

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

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

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

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

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

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

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

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

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

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

281 

282 # Change logging level 

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

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

285 pass 

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

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

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

289 

290 # Use a new logger with a message. 

291 msg = "Test message %d" 

292 test_num = 42 

293 logname = "test" 

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

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

296 pass 

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

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

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

300 

301 # Prefix the logger. 

302 prefix = "prefix" 

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

304 with time_this(prefix=prefix): 

305 pass 

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

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

308 

309 # Prefix explicit logger. 

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

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

312 pass 

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

314 

315 # Trigger a problem. 

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

317 with self.assertRaises(RuntimeError): 

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

319 raise RuntimeError("A problem") 

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

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

322 

323 

324class ProfileTestCase(unittest.TestCase): 

325 """Test profiling decorator.""" 

326 

327 def test_profile(self): 

328 logger = logging.getLogger("profile") 

329 

330 with profile(None) as prof: 

331 pass 

332 self.assertIsNone(prof) 

333 

334 with tempfile.NamedTemporaryFile() as tmp: 

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

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

337 pass 

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

339 self.assertIsNotNone(prof) 

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

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

342 

343 

344if __name__ == "__main__": 

345 unittest.main()