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

113 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 19:55 +0000

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 click 

28from collections import namedtuple 

29from functools import partial 

30from io import StringIO 

31import logging 

32import re 

33import subprocess 

34import unittest 

35import tempfile 

36 

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 clickResultMsg, command_test_env, LogCliRunner 

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

41try: 

42 import lsst.log as lsstLog 

43except ModuleNotFoundError: 

44 lsstLog = None 

45 

46 

47@click.command() 

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

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

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

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

52def command_log_settings_test(expected_pyroot_level, 

53 expected_pybutler_level, 

54 expected_lsstroot_level, 

55 expected_lsstbutler_level): 

56 

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

58 

59 logLevels = [LogLevel(expected_pyroot_level, 

60 logging.getLogger().level, 

61 "pyRoot"), 

62 LogLevel(expected_pybutler_level, 

63 logging.getLogger("lsst.daf.butler").level, 

64 "pyButler")] 

65 if lsstLog is not None: 

66 logLevels.extend([LogLevel(expected_lsstroot_level, 

67 lsstLog.getLogger("").getLevel(), 

68 "lsstRoot"), 

69 LogLevel(expected_lsstbutler_level, 

70 lsstLog.getLogger("lsst.daf.butler").getLevel(), 

71 "lsstButler")]) 

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 def __init__(self, component): 

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

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

102 

103 def runTest(self, cmd): 

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

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

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

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

108 pyRoot = self.PythonLogger(None) 

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

110 lsstRoot = self.LsstLogger("") 

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

112 

113 with command_test_env(self.runner, "lsst.daf.butler.tests.cliLogTestBase", 

114 "command-log-settings-test"): 

115 result = cmd() 

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

117 

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

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

120 if lsstLog is not None: 

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

122 # lsstLogLevel can either be the inital level, or uninitialized or 

123 # the defined default value. 

124 expectedLsstLogLevel = ((lsstButler.initialLevel, ) if lsstButler.initialLevel != -1 

125 else(-1, CliLog.defaultLsstLogLevel)) 

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

127 

128 def test_butlerCliLog(self): 

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

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

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

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

133 

134 self.runTest(partial(self.runner.invoke, 

135 butlerCli, 

136 ["--log-level", "WARNING", 

137 "--log-level", "lsst.daf.butler=DEBUG", 

138 "command-log-settings-test", 

139 "--expected-pyroot-level", logging.WARNING, 

140 "--expected-pybutler-level", logging.DEBUG, 

141 "--expected-lsstroot-level", lsstLog.WARN if lsstLog else 0, 

142 "--expected-lsstbutler-level", lsstLog.DEBUG if lsstLog else 0])) 

143 

144 def test_helpLogReset(self): 

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

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

147 

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

149 

150 def testLongLog(self): 

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

152 flag is set.""" 

153 

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

155 # timestamp with the following format: 

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

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

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

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

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

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

162 # expected. 

163 timestampRegex = re.compile( 

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

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

166 

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

168 # log level, for example: 

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

170 modulesRegex = re.compile( 

171 r".* ([a-z]+\.)+[a-z]+ [A-Z]+: .*") 

172 

173 with self.runner.isolated_filesystem(): 

174 for longlog in (True, False): 

175 # The pytest log handler interferes with the log configuration 

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

177 # a subprocess. 

178 if longlog: 

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

180 else: 

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

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

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

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

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

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

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

188 output.seek(0) 

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

190 if longlog: 

191 self.assertTrue(startedWithTimestamp, 

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

193 self.assertFalse(startedWithModule, 

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

195 else: 

196 self.assertFalse(startedWithTimestamp, 

197 msg=f"found timestamp in: \n{output.getvalue()}") 

198 self.assertTrue(startedWithModule, 

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

200 

201 def testFileLogging(self): 

202 """Test --log-file option.""" 

203 with self.runner.isolated_filesystem(): 

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

205 # Get a temporary file name and immediately close it 

206 fd = tempfile.NamedTemporaryFile(suffix=suffix) 

207 filename = fd.name 

208 fd.close() 

209 

210 args = ("--log-level", "DEBUG", "--log-file", filename, 

211 "--log-label", "k1=v1,k2=v2", "--log-label", "k3=v3", 

212 "create", f"here{i}") 

213 

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

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

216 

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

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

219 # python itself since warnings are redirected to log 

220 # messages. 

221 num = 4 

222 

223 if suffix == ".json": 

224 records = ButlerLogRecords.from_file(filename) 

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

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

227 else: 

228 with open(filename) as fd: 

229 records = fd.readlines() 

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

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

232 

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

234 

235 def testLogTty(self): 

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

237 

238 with self.runner.isolated_filesystem(): 

239 for log_tty in (True, False): 

240 # The pytest log handler interferes with the log configuration 

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

242 # a subprocess. 

243 if log_tty: 

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

245 else: 

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

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

248 

249 output = result.stderr.decode() 

250 if log_tty: 

251 self.assertIn("DEBUG", output) 

252 else: 

253 self.assertNotIn("DEBUG", output) 

254 

255 

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

257 unittest.main()