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

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
35import tempfile
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
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):
57 LogLevel = namedtuple("LogLevel", ("expected", "actual", "name"))
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}"))
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."""
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
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")
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))
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)
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."""
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]))
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."""
148 self.runTest(partial(self.runner.invoke, butlerCli, ["command-log-settings-test", "--help"]))
150 def testLongLog(self):
151 """Verify the timestamp is in the log messages when the --long-log
152 flag is set."""
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) .*")
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]+: .*")
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()}")
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()
210 args = ("--log-level", "DEBUG", "--log-file", filename,
211 "--log-label", "k1=v1,k2=v2", "--log-label", "k3=v3",
212 "create", f"here{i}")
214 result = self.runner.invoke(butlerCli, args)
215 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
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
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]))
233 self.assertGreater(len(records), 5)
235 def testLogTty(self):
236 """Verify that log output to terminal can be suppressed."""
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)
249 output = result.stderr.decode()
250 if log_tty:
251 self.assertIn("DEBUG", output)
252 else:
253 self.assertNotIn("DEBUG", output)
256if __name__ == "__main__": 256 ↛ 257line 256 didn't jump to line 257, because the condition on line 256 was never true
257 unittest.main()