Hide keyboard shortcuts

Hot-keys 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

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 

35 

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

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

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

39try: 

40 import lsst.log as lsstLog 

41except ModuleNotFoundError: 

42 lsstLog = None 

43 

44 

45def hasLsstLogHandler(logger): 

46 """Check if a python logger has an lsst.log.LogHandler installed. 

47 

48 Parameters 

49 ---------- 

50 logger : `logging.logger` 

51 A python logger. 

52 

53 Returns 

54 ------ 

55 `bool` 

56 True if the logger has an lsst.log.LogHander installed, else False. 

57 """ 

58 if lsstLog is None: 

59 return False 

60 for handler in logging.getLogger().handlers: 

61 if isinstance(handler, lsstLog.LogHandler): 

62 return True 

63 

64 

65@click.command() 

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

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

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

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

70def command_log_settings_test(expected_pyroot_level, 

71 expected_pybutler_level, 

72 expected_lsstroot_level, 

73 expected_lsstbutler_level): 

74 if lsstLog is not None and not hasLsstLogHandler(logging.getLogger()): 

75 raise click.ClickException("Expected to find an lsst.log handler in the python root logger's " 

76 "handlers.") 

77 

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

79 

80 logLevels = [LogLevel(expected_pyroot_level, 

81 logging.getLogger().level, 

82 "pyRoot"), 

83 LogLevel(expected_pybutler_level, 

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

85 "pyButler")] 

86 if lsstLog is not None: 

87 logLevels.extend([LogLevel(expected_lsstroot_level, 

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

89 "lsstRoot"), 

90 LogLevel(expected_lsstbutler_level, 

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

92 "lsstButler")]) 

93 for expected, actual, name in logLevels: 

94 if expected != actual: 

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

96 

97 

98class CliLogTestBase(): 

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

100 

101 lsstLogHandlerId = None 

102 

103 def setUp(self): 

104 self.runner = LogCliRunner() 

105 

106 def tearDown(self): 

107 self.lsstLogHandlerId = None 

108 

109 class PythonLogger: 

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

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

112 

113 def __init__(self, component): 

114 self.logger = logging.getLogger(component) 

115 self.initialLevel = self.logger.level 

116 

117 class LsstLogger: 

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

119 initialized.""" 

120 def __init__(self, component): 

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

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

123 

124 def runTest(self, cmd): 

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

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

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

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

129 pyRoot = self.PythonLogger(None) 

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

131 lsstRoot = self.LsstLogger("") 

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

133 

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

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

136 result = cmd() 

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

138 

139 if lsstLog is not None: 

140 self.assertFalse(hasLsstLogHandler(logging.getLogger()), 

141 msg="CliLog should remove the lsst.log handler it added to the root logger.") 

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

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

144 if lsstLog is not None: 

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

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

147 # the defined default value. 

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

149 else(-1, CliLog.defaultLsstLogLevel)) 

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

151 

152 def test_butlerCliLog(self): 

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

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

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

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

157 

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

159 butlerCli, 

160 ["--log-level", "WARNING", 

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

162 "command-log-settings-test", 

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

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

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

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

167 

168 def test_helpLogReset(self): 

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

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

171 

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

173 

174 def testLongLog(self): 

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

176 flag is set.""" 

177 

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

179 # timestamp with the following format: 

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

181 # If lsst.log is importable then the timestamp will have 

182 # milliseconds, as described above. If lsst.log is NOT 

183 # importable then milliseconds (and the preceding ".") are 

184 # omitted (the python `time` module does not support 

185 # milliseconds in its format string). Examples of expected 

186 # strings follow: 

187 # lsst.log: "DEBUG 2020-10-29T10:20:31.518-0700 ..." 

188 # pure python "DEBUG 2020-10-28T10:20:31-0700 ..."" 

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

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

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

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

193 # expected. 

194 # N.B. this test is defined in daf_butler which does not depend 

195 # on lsst.log. However, CliLog may be used in packages that do 

196 # depend on lsst.log and so both forms of timestamps must be 

197 # supported. These packages should have a test (the file is 

198 # usually called test_cliLog.py) that subclasses CliLogTestBase 

199 # and unittest.TestCase so that these tests are run in that 

200 # package. 

201 timestampRegex = re.compile( 

202 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})?([-,+][0-9]{4}|Z) .*") 

203 

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

205 # log level, for example: 

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

207 modulesRegex = re.compile( 

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

209 

210 with self.runner.isolated_filesystem(): 

211 for longlog in (True, False): 

212 # The click test does not capture logging emitted from lsst.log 

213 # so use subprocess to run the test instead. 

214 if longlog: 

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

216 else: 

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

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

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

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

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

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

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

224 output.seek(0) 

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

226 if longlog: 

227 self.assertTrue(startedWithTimestamp, 

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

229 self.assertFalse(startedWithModule, 

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

231 else: 

232 self.assertFalse(startedWithTimestamp, 

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

234 self.assertTrue(startedWithModule, 

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

236 

237 

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

239 unittest.main()