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
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
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 logging
28import re
29import subprocess
30import tempfile
31import unittest
32from collections import namedtuple
33from functools import partial
34from io import StringIO
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
42try:
43 import lsst.log as lsstLog
44except ModuleNotFoundError:
45 lsstLog = None
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):
57 LogLevel = namedtuple("LogLevel", ("expected", "actual", "name"))
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}"))
77class CliLogTestBase:
78 """Tests log initialization, reset, and setting log levels."""
80 lsstLogHandlerId = None
82 def setUp(self):
83 self.runner = LogCliRunner()
85 def tearDown(self):
86 self.lsstLogHandlerId = None
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."""
92 def __init__(self, component):
93 self.logger = logging.getLogger(component)
94 self.initialLevel = self.logger.level
96 class LsstLogger:
97 """Keeps track of log level for a component at the time this object was
98 initialized."""
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
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")
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))
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)
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."""
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 )
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."""
165 self.runTest(partial(self.runner.invoke, butlerCli, ["command-log-settings-test", "--help"]))
167 def testLongLog(self):
168 """Verify the timestamp is in the log messages when the --long-log
169 flag is set."""
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 )
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]+: .*")
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 )
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()
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 )
243 result = self.runner.invoke(butlerCli, args)
244 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
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
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]))
262 self.assertGreater(len(records), 5)
264 def testLogTty(self):
265 """Verify that log output to terminal can be suppressed."""
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)
278 output = result.stderr.decode()
279 if log_tty:
280 self.assertIn("DEBUG", output)
281 else:
282 self.assertNotIn("DEBUG", output)
285if __name__ == "__main__": 285 ↛ 286line 285 didn't jump to line 286, because the condition on line 285 was never true
286 unittest.main()