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, "create", f"here{i}")
212 result = self.runner.invoke(butlerCli, args)
213 self.assertEqual(result.exit_code, 0, clickResultMsg(result))
215 # Record to test. Test one in the middle that we know is
216 # a DEBUG message. The first message might come from
217 # python itself since warnings are redirected to log
218 # messages.
219 num = 4
221 if suffix == ".json":
222 records = ButlerLogRecords.from_file(filename)
223 self.assertEqual(records[num].levelname, "DEBUG", str(records[num]))
224 else:
225 with open(filename) as fd:
226 records = fd.readlines()
227 self.assertIn("DEBUG", records[num], str(records[num]))
228 self.assertNotIn("{", records[num], str(records[num]))
230 self.assertGreater(len(records), 5)
232 def testLogTty(self):
233 """Verify that log output to terminal can be suppressed."""
235 with self.runner.isolated_filesystem():
236 for log_tty in (True, False):
237 # The pytest log handler interferes with the log configuration
238 # settings set up by initLog -- therefore test by using
239 # a subprocess.
240 if log_tty:
241 args = ("butler", "--log-level", "DEBUG", "--log-tty", "create", "here")
242 else:
243 args = ("butler", "--log-level", "DEBUG", "--no-log-tty", "create", "here2")
244 result = subprocess.run(args, capture_output=True)
246 output = result.stderr.decode()
247 if log_tty:
248 self.assertIn("DEBUG", output)
249 else:
250 self.assertNotIn("DEBUG", output)
253if __name__ == "__main__": 253 ↛ 254line 253 didn't jump to line 254, because the condition on line 253 was never true
254 unittest.main()