Coverage for python/lsst/daf/butler/tests/cliLogTestBase.py: 30%

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

136 statements  

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21 

22"""Unit tests for the daf_butler CliLog utility. Code is implemented in 

23daf_butler but some only runs if lsst.log.Log can be imported so these parts of 

24it can't be tested there because daf_butler does not directly depend on 

25lsst.log, and only uses it if it has been setup by another package.""" 

26 

27import logging 

28import os 

29import re 

30import subprocess 

31import tempfile 

32import unittest 

33from collections import namedtuple 

34from functools import partial 

35from io import StringIO 

36from logging import DEBUG, INFO, WARNING 

37 

38import click 

39from lsst.daf.butler.cli.butler import cli as butlerCli 

40from lsst.daf.butler.cli.cliLog import CliLog 

41from lsst.daf.butler.cli.utils import LogCliRunner, clickResultMsg, command_test_env 

42from lsst.daf.butler.core.logging import ButlerLogRecords 

43from lsst.utils.logging import TRACE 

44 

45try: 

46 import lsst.log as lsstLog 

47 

48 lsstLog_INFO = lsstLog.INFO 

49 lsstLog_DEBUG = lsstLog.DEBUG 

50 lsstLog_WARN = lsstLog.WARN 

51except ModuleNotFoundError: 

52 lsstLog = None 

53 lsstLog_INFO = 0 

54 lsstLog_DEBUG = 0 

55 lsstLog_WARN = 0 

56 

57 

58@click.command() 

59@click.option("--expected-pyroot-level", type=int) 

60@click.option("--expected-pylsst-level", type=int) 

61@click.option("--expected-pybutler-level", type=int) 

62@click.option("--expected-lsstroot-level", type=int) 

63@click.option("--expected-lsstbutler-level", type=int) 

64@click.option("--expected-lsstx-level", type=int) 

65def command_log_settings_test( 

66 expected_pyroot_level, 

67 expected_pylsst_level, 

68 expected_pybutler_level, 

69 expected_lsstroot_level, 

70 expected_lsstbutler_level, 

71 expected_lsstx_level, 

72): 

73 

74 LogLevel = namedtuple("LogLevel", ("expected", "actual", "name")) 

75 

76 logLevels = [ 

77 LogLevel(expected_pyroot_level, logging.getLogger().level, "pyRoot"), 

78 LogLevel(expected_pylsst_level, logging.getLogger("lsst").getEffectiveLevel(), "pyLsst"), 

79 LogLevel( 

80 expected_pybutler_level, logging.getLogger("lsst.daf.butler").getEffectiveLevel(), "pyButler" 

81 ), 

82 LogLevel(expected_lsstx_level, logging.getLogger("lsstx").getEffectiveLevel(), "pyLsstx"), 

83 ] 

84 if lsstLog is not None: 

85 logLevels.extend( 

86 [ 

87 LogLevel(expected_lsstroot_level, lsstLog.getLogger("lsst").getEffectiveLevel(), "lsstRoot"), 

88 LogLevel( 

89 expected_lsstbutler_level, 

90 lsstLog.getLogger("lsst.daf.butler").getEffectiveLevel(), 

91 "lsstButler", 

92 ), 

93 ] 

94 ) 

95 for expected, actual, name in logLevels: 

96 if expected != actual: 

97 raise (click.ClickException(f"expected {name} level to be {expected!r}, actual:{actual!r}")) 

98 

99 

100class CliLogTestBase: 

101 """Tests log initialization, reset, and setting log levels.""" 

102 

103 lsstLogHandlerId = None 

104 

105 def setUp(self): 

106 self.runner = LogCliRunner() 

107 

108 def tearDown(self): 

109 self.lsstLogHandlerId = None 

110 

111 class PythonLogger: 

112 """Keeps track of log level of a component and number of handlers 

113 attached to it at the time this object was initialized.""" 

114 

115 def __init__(self, component): 

116 self.logger = logging.getLogger(component) 

117 self.initialLevel = self.logger.level 

118 

119 class LsstLogger: 

120 """Keeps track of log level for a component at the time this object was 

121 initialized.""" 

122 

123 def __init__(self, component): 

124 self.logger = lsstLog.getLogger(component) if lsstLog else None 

125 self.initialLevel = self.logger.getLevel() if lsstLog else None 

126 

127 def runTest(self, cmd): 

128 """Test that the log context manager works with the butler cli to 

129 initialize the logging system according to cli inputs for the duration 

130 of the command execution and resets the logging system to its previous 

131 state or expected state when command execution finishes.""" 

132 pyRoot = self.PythonLogger(None) 

133 pyButler = self.PythonLogger("lsst.daf.butler") 

134 pyLsstRoot = self.PythonLogger("lsst") 

135 lsstRoot = self.LsstLogger("") 

136 lsstButler = self.LsstLogger("lsst.daf.butler") 

137 

138 with command_test_env( 

139 self.runner, "lsst.daf.butler.tests.cliLogTestBase", "command-log-settings-test" 

140 ): 

141 result = cmd() 

142 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

143 

144 self.assertEqual(pyRoot.logger.level, logging.WARNING) 

145 self.assertEqual(pyLsstRoot.logger.level, logging.INFO) 

146 self.assertEqual(pyButler.logger.level, pyButler.initialLevel) 

147 if lsstLog is not None: 

148 self.assertEqual(lsstRoot.logger.getLevel(), lsstLog.INFO) 

149 # lsstLogLevel can either be the initial level, or uninitialized or 

150 # the defined default value. 

151 expectedLsstLogLevel = ( 

152 (lsstButler.initialLevel,) 

153 if lsstButler.initialLevel != -1 

154 else (-1, CliLog.defaultLsstLogLevel) 

155 ) 

156 self.assertIn(lsstButler.logger.getLevel(), expectedLsstLogLevel) 

157 

158 def test_butlerCliLog(self): 

159 """Test that the log context manager works with the butler cli to 

160 initialize the logging system according to cli inputs for the duration 

161 of the command execution and resets the logging system to its previous 

162 state or expected state when command execution finishes.""" 

163 

164 # Run with two different log level settings. 

165 log_levels = ( 

166 # --log-level / --log-level / expected pyroot, pylsst, pybutler, 

167 # lsstroot, lsstbutler, lsstx 

168 ( 

169 "WARNING", 

170 "lsst.daf.butler=DEBUG", 

171 WARNING, 

172 WARNING, 

173 DEBUG, 

174 lsstLog_WARN, 

175 lsstLog_DEBUG, 

176 WARNING, 

177 ), 

178 ("DEBUG", "lsst.daf.butler=TRACE", WARNING, DEBUG, TRACE, lsstLog_DEBUG, lsstLog_DEBUG, WARNING), 

179 (".=DEBUG", "lsst.daf.butler=WARNING", DEBUG, INFO, WARNING, lsstLog_INFO, lsstLog_WARN, DEBUG), 

180 (".=DEBUG", "DEBUG", DEBUG, DEBUG, DEBUG, lsstLog_DEBUG, lsstLog_DEBUG, DEBUG), 

181 (".=DEBUG", "conda=DEBUG", DEBUG, INFO, INFO, lsstLog_INFO, lsstLog_INFO, DEBUG), 

182 ) 

183 

184 self._test_levels(log_levels) 

185 

186 # Check that the environment variable can set additional roots. 

187 log_levels = ( 

188 # --log-level / --log-level / expected pyroot, pylsst, pybutler, 

189 # lsstroot, lsstbutler, lsstx 

190 ( 

191 "WARNING", 

192 "lsst.daf.butler=DEBUG", 

193 WARNING, 

194 WARNING, 

195 DEBUG, 

196 lsstLog_WARN, 

197 lsstLog_DEBUG, 

198 WARNING, 

199 ), 

200 ("DEBUG", "lsst.daf.butler=TRACE", WARNING, DEBUG, TRACE, lsstLog_DEBUG, lsstLog_DEBUG, DEBUG), 

201 (".=DEBUG", "lsst.daf.butler=WARNING", DEBUG, INFO, WARNING, lsstLog_INFO, lsstLog_WARN, INFO), 

202 (".=DEBUG", "DEBUG", DEBUG, DEBUG, DEBUG, lsstLog_DEBUG, lsstLog_DEBUG, DEBUG), 

203 (".=DEBUG", "conda=DEBUG", DEBUG, INFO, INFO, lsstLog_INFO, lsstLog_INFO, INFO), 

204 ) 

205 

206 with unittest.mock.patch.dict(os.environ, {"DAF_BUTLER_ROOT_LOGGER": "lsstx"}): 

207 self._test_levels(log_levels) 

208 

209 def _test_levels(self, log_levels): 

210 for level1, level2, x_pyroot, x_pylsst, x_pybutler, x_lsstroot, x_lsstbutler, x_lsstx in log_levels: 

211 with self.subTest("Test different log levels", level1=level1, level2=level2): 

212 self.runTest( 

213 partial( 

214 self.runner.invoke, 

215 butlerCli, 

216 [ 

217 "--log-level", 

218 level1, 

219 "--log-level", 

220 level2, 

221 "command-log-settings-test", 

222 "--expected-pyroot-level", 

223 x_pyroot, 

224 "--expected-pylsst-level", 

225 x_pylsst, 

226 "--expected-pybutler-level", 

227 x_pybutler, 

228 "--expected-lsstroot-level", 

229 x_lsstroot, 

230 "--expected-lsstbutler-level", 

231 x_lsstbutler, 

232 "--expected-lsstx-level", 

233 x_lsstx, 

234 ], 

235 ) 

236 ) 

237 

238 def test_helpLogReset(self): 

239 """Verify that when a command does not execute, like when the help menu 

240 is printed instead, that CliLog is still reset.""" 

241 

242 self.runTest(partial(self.runner.invoke, butlerCli, ["command-log-settings-test", "--help"])) 

243 

244 def testLongLog(self): 

245 """Verify the timestamp is in the log messages when the --long-log 

246 flag is set.""" 

247 

248 # When longlog=True, loglines start with the log level and a 

249 # timestamp with the following format: 

250 # "year-month-day T hour-minute-second.millisecond-zoneoffset" 

251 # For example: "DEBUG 2020-10-28T10:20:31-07:00 ..."" 

252 # The log level name can change, we verify there is an all 

253 # caps word there but do not verify the word. We do not verify 

254 # the rest of the log string, assume that if the timestamp is 

255 # in the string that the rest of the string will appear as 

256 # expected. 

257 timestampRegex = re.compile( 

258 r".*[A-Z]+ [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]{3})" 

259 "?([-,+][01][0-9]:[034][05]|Z) .*" 

260 ) 

261 

262 # When longlog=False, log lines start with the module name and 

263 # log level, for example: 

264 # lsst.daf.butler.core.config DEBUG: ... 

265 modulesRegex = re.compile(r".* ([a-z]+\.)+[a-z]+ [A-Z]+: .*") 

266 

267 with self.runner.isolated_filesystem(): 

268 for longlog in (True, False): 

269 # The pytest log handler interferes with the log configuration 

270 # settings set up by initLog -- therefore test by using 

271 # a subprocess. 

272 if longlog: 

273 args = ("butler", "--log-level", "DEBUG", "--long-log", "create", "here") 

274 else: 

275 args = ("butler", "--log-level", "DEBUG", "create", "here") 

276 result = subprocess.run(args, capture_output=True) 

277 # There are cases where the newlines are stripped from the log 

278 # output (like in Jenkins), since we can't depend on newlines 

279 # in log output they are removed here from test output. 

280 output = StringIO((result.stderr.decode().replace("\n", " "))) 

281 startedWithTimestamp = any([timestampRegex.match(line) for line in output.readlines()]) 

282 output.seek(0) 

283 startedWithModule = any(modulesRegex.match(line) for line in output.readlines()) 

284 if longlog: 

285 self.assertTrue( 

286 startedWithTimestamp, msg=f"did not find timestamp in: \n{output.getvalue()}" 

287 ) 

288 self.assertFalse( 

289 startedWithModule, msg=f"found lines starting with module in: \n{output.getvalue()}" 

290 ) 

291 else: 

292 self.assertFalse(startedWithTimestamp, msg=f"found timestamp in: \n{output.getvalue()}") 

293 self.assertTrue( 

294 startedWithModule, 

295 msg=f"did not find lines starting with module in: \n{output.getvalue()}", 

296 ) 

297 

298 def testFileLogging(self): 

299 """Test --log-file option.""" 

300 with self.runner.isolated_filesystem(): 

301 for i, suffix in enumerate([".json", ".log"]): 

302 # Get a temporary file name and immediately close it 

303 fd = tempfile.NamedTemporaryFile(suffix=suffix) 

304 filename = fd.name 

305 fd.close() 

306 

307 args = ( 

308 "--log-level", 

309 "DEBUG", 

310 "--log-file", 

311 filename, 

312 "--log-label", 

313 "k1=v1,k2=v2", 

314 "--log-label", 

315 "k3=v3", 

316 "create", 

317 f"here{i}", 

318 ) 

319 

320 result = self.runner.invoke(butlerCli, args) 

321 self.assertEqual(result.exit_code, 0, clickResultMsg(result)) 

322 

323 # Record to test. Test one in the middle that we know is 

324 # a DEBUG message. The first message might come from 

325 # python itself since warnings are redirected to log 

326 # messages. 

327 num = 4 

328 

329 if suffix == ".json": 

330 records = ButlerLogRecords.from_file(filename) 

331 self.assertGreater(len(records), num) 

332 self.assertEqual(records[num].levelname, "DEBUG", str(records[num])) 

333 self.assertEqual(records[0].MDC, dict(K1="v1", K2="v2", K3="v3")) 

334 else: 

335 with open(filename) as fd: 

336 records = fd.readlines() 

337 self.assertGreater(len(records), num) 

338 self.assertIn("DEBUG", records[num], str(records[num])) 

339 self.assertNotIn("{", records[num], str(records[num])) 

340 

341 self.assertGreater(len(records), 5) 

342 

343 def testLogTty(self): 

344 """Verify that log output to terminal can be suppressed.""" 

345 

346 with self.runner.isolated_filesystem(): 

347 for log_tty in (True, False): 

348 # The pytest log handler interferes with the log configuration 

349 # settings set up by initLog -- therefore test by using 

350 # a subprocess. 

351 if log_tty: 

352 args = ("butler", "--log-level", "DEBUG", "--log-tty", "create", "here") 

353 else: 

354 args = ("butler", "--log-level", "DEBUG", "--no-log-tty", "create", "here2") 

355 result = subprocess.run(args, capture_output=True) 

356 

357 output = result.stderr.decode() 

358 if log_tty: 

359 self.assertIn("DEBUG", output) 

360 else: 

361 self.assertNotIn("DEBUG", output) 

362 

363 

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

365 unittest.main()