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 

45@click.command() 

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

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

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

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

50def command_log_settings_test(expected_pyroot_level, 

51 expected_pybutler_level, 

52 expected_lsstroot_level, 

53 expected_lsstbutler_level): 

54 

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

56 

57 logLevels = [LogLevel(expected_pyroot_level, 

58 logging.getLogger().level, 

59 "pyRoot"), 

60 LogLevel(expected_pybutler_level, 

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

62 "pyButler")] 

63 if lsstLog is not None: 

64 logLevels.extend([LogLevel(expected_lsstroot_level, 

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

66 "lsstRoot"), 

67 LogLevel(expected_lsstbutler_level, 

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

69 "lsstButler")]) 

70 for expected, actual, name in logLevels: 

71 if expected != actual: 

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

73 

74 

75class CliLogTestBase(): 

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

77 

78 lsstLogHandlerId = None 

79 

80 def setUp(self): 

81 self.runner = LogCliRunner() 

82 

83 def tearDown(self): 

84 self.lsstLogHandlerId = None 

85 

86 class PythonLogger: 

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

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

89 

90 def __init__(self, component): 

91 self.logger = logging.getLogger(component) 

92 self.initialLevel = self.logger.level 

93 

94 class LsstLogger: 

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

96 initialized.""" 

97 def __init__(self, component): 

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

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

100 

101 def runTest(self, cmd): 

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

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

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

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

106 pyRoot = self.PythonLogger(None) 

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

108 lsstRoot = self.LsstLogger("") 

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

110 

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

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

113 result = cmd() 

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

115 

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

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

118 if lsstLog is not None: 

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

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

121 # the defined default value. 

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

123 else(-1, CliLog.defaultLsstLogLevel)) 

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

125 

126 def test_butlerCliLog(self): 

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

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

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

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

131 

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

133 butlerCli, 

134 ["--log-level", "WARNING", 

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

136 "command-log-settings-test", 

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

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

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

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

141 

142 def test_helpLogReset(self): 

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

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

145 

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

147 

148 def testLongLog(self): 

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

150 flag is set.""" 

151 

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

153 # timestamp with the following format: 

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

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

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

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

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

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

160 # expected. 

161 timestampRegex = re.compile( 

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

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

164 

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

166 # log level, for example: 

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

168 modulesRegex = re.compile( 

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

170 

171 with self.runner.isolated_filesystem(): 

172 for longlog in (True, False): 

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

174 # so use subprocess to run the test instead. 

175 if longlog: 

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

177 else: 

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

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

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

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

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

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

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

185 output.seek(0) 

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

187 if longlog: 

188 self.assertTrue(startedWithTimestamp, 

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

190 self.assertFalse(startedWithModule, 

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

192 else: 

193 self.assertFalse(startedWithTimestamp, 

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

195 self.assertTrue(startedWithModule, 

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

197 

198 

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

200 unittest.main()