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

113 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 re 

29import subprocess 

30import tempfile 

31import unittest 

32from collections import namedtuple 

33from functools import partial 

34from io import StringIO 

35 

36import click 

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

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

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

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

41 

42try: 

43 import lsst.log as lsstLog 

44except ModuleNotFoundError: 

45 lsstLog = None 

46 

47 

48@click.command() 

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

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

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

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

53def command_log_settings_test( 

54 expected_pyroot_level, expected_pybutler_level, expected_lsstroot_level, expected_lsstbutler_level 

55): 

56 

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

58 

59 logLevels = [ 

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

61 LogLevel(expected_pybutler_level, logging.getLogger("lsst.daf.butler").level, "pyButler"), 

62 ] 

63 if lsstLog is not None: 

64 logLevels.extend( 

65 [ 

66 LogLevel(expected_lsstroot_level, lsstLog.getLogger("").getLevel(), "lsstRoot"), 

67 LogLevel( 

68 expected_lsstbutler_level, lsstLog.getLogger("lsst.daf.butler").getLevel(), "lsstButler" 

69 ), 

70 ] 

71 ) 

72 for expected, actual, name in logLevels: 

73 if expected != actual: 

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

75 

76 

77class CliLogTestBase: 

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

79 

80 lsstLogHandlerId = None 

81 

82 def setUp(self): 

83 self.runner = LogCliRunner() 

84 

85 def tearDown(self): 

86 self.lsstLogHandlerId = None 

87 

88 class PythonLogger: 

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

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

91 

92 def __init__(self, component): 

93 self.logger = logging.getLogger(component) 

94 self.initialLevel = self.logger.level 

95 

96 class LsstLogger: 

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

98 initialized.""" 

99 

100 def __init__(self, component): 

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

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

103 

104 def runTest(self, cmd): 

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

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

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

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

109 pyRoot = self.PythonLogger(None) 

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

111 lsstRoot = self.LsstLogger("") 

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

113 

114 with command_test_env( 

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

116 ): 

117 result = cmd() 

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

119 

120 self.assertEqual(pyRoot.logger.level, logging.INFO) 

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

122 if lsstLog is not None: 

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

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

125 # the defined default value. 

126 expectedLsstLogLevel = ( 

127 (lsstButler.initialLevel,) 

128 if lsstButler.initialLevel != -1 

129 else (-1, CliLog.defaultLsstLogLevel) 

130 ) 

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

132 

133 def test_butlerCliLog(self): 

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

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

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

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

138 

139 self.runTest( 

140 partial( 

141 self.runner.invoke, 

142 butlerCli, 

143 [ 

144 "--log-level", 

145 "WARNING", 

146 "--log-level", 

147 "lsst.daf.butler=DEBUG", 

148 "command-log-settings-test", 

149 "--expected-pyroot-level", 

150 logging.WARNING, 

151 "--expected-pybutler-level", 

152 logging.DEBUG, 

153 "--expected-lsstroot-level", 

154 lsstLog.WARN if lsstLog else 0, 

155 "--expected-lsstbutler-level", 

156 lsstLog.DEBUG if lsstLog else 0, 

157 ], 

158 ) 

159 ) 

160 

161 def test_helpLogReset(self): 

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

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

164 

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

166 

167 def testLongLog(self): 

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

169 flag is set.""" 

170 

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

172 # timestamp with the following format: 

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

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

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

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

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

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

179 # expected. 

180 timestampRegex = re.compile( 

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

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

183 ) 

184 

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

186 # log level, for example: 

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

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

189 

190 with self.runner.isolated_filesystem(): 

191 for longlog in (True, False): 

192 # The pytest log handler interferes with the log configuration 

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

194 # a subprocess. 

195 if longlog: 

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

197 else: 

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

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

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

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

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

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

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

205 output.seek(0) 

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

207 if longlog: 

208 self.assertTrue( 

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

210 ) 

211 self.assertFalse( 

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

213 ) 

214 else: 

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

216 self.assertTrue( 

217 startedWithModule, 

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

219 ) 

220 

221 def testFileLogging(self): 

222 """Test --log-file option.""" 

223 with self.runner.isolated_filesystem(): 

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

225 # Get a temporary file name and immediately close it 

226 fd = tempfile.NamedTemporaryFile(suffix=suffix) 

227 filename = fd.name 

228 fd.close() 

229 

230 args = ( 

231 "--log-level", 

232 "DEBUG", 

233 "--log-file", 

234 filename, 

235 "--log-label", 

236 "k1=v1,k2=v2", 

237 "--log-label", 

238 "k3=v3", 

239 "create", 

240 f"here{i}", 

241 ) 

242 

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

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

245 

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

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

248 # python itself since warnings are redirected to log 

249 # messages. 

250 num = 4 

251 

252 if suffix == ".json": 

253 records = ButlerLogRecords.from_file(filename) 

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

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

256 else: 

257 with open(filename) as fd: 

258 records = fd.readlines() 

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

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

261 

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

263 

264 def testLogTty(self): 

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

266 

267 with self.runner.isolated_filesystem(): 

268 for log_tty 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 log_tty: 

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

274 else: 

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

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

277 

278 output = result.stderr.decode() 

279 if log_tty: 

280 self.assertIn("DEBUG", output) 

281 else: 

282 self.assertNotIn("DEBUG", output) 

283 

284 

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

286 unittest.main()