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

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/>.
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."""
27import click
28from collections import namedtuple
29from functools import partial
30from io import StringIO
31import logging
32import re
33import subprocess
34import unittest
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
45def hasLsstLogHandler(logger):
46 """Check if a python logger has an lsst.log.LogHandler installed.
48 Parameters
49 ----------
50 logger : `logging.logger`
51 A python logger.
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
65@click.command()
66@click.option("--expected-pyroot-level")
67@click.option("--expected-pybutler-level")
68@click.option("--expected-lsstroot-level")
69@click.option("--expected-lsstbutler-level")
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.")
78 LogLevel = namedtuple("LogLevel", ("expected", "actual", "name"))
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}, actual:{actual}"))
98class CliLogTestBase():
99 """Tests log initialization, reset, and setting log levels."""
101 lsstLogHandlerId = None
103 def setUp(self):
104 self.runner = LogCliRunner()
106 def tearDown(self):
107 self.lsstLogHandlerId = None
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."""
113 def __init__(self, component):
114 self.logger = logging.getLogger(component)
115 self.initialLevel = self.logger.level
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
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")
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))
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)
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."""
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]))
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."""
172 self.runTest(partial(self.runner.invoke, butlerCli, ["command-log-settings-test", "--help"]))
174 def testLongLog(self):
175 """Verify the timestamp is in the log messages when the --long-log
176 flag is set."""
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) .*")
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]+: .*")
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()}")
238if __name__ == "__main__": 238 ↛ 239line 238 didn't jump to line 239, because the condition on line 238 was never true
239 unittest.main()