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

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
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):
55 LogLevel = namedtuple("LogLevel", ("expected", "actual", "name"))
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}"))
75class CliLogTestBase():
76 """Tests log initialization, reset, and setting log levels."""
78 lsstLogHandlerId = None
80 def setUp(self):
81 self.runner = LogCliRunner()
83 def tearDown(self):
84 self.lsstLogHandlerId = None
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."""
90 def __init__(self, component):
91 self.logger = logging.getLogger(component)
92 self.initialLevel = self.logger.level
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
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")
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))
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)
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."""
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]))
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."""
146 self.runTest(partial(self.runner.invoke, butlerCli, ["command-log-settings-test", "--help"]))
148 def testLongLog(self):
149 """Verify the timestamp is in the log messages when the --long-log
150 flag is set."""
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) .*")
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]+: .*")
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()}")
199if __name__ == "__main__": 199 ↛ 200line 199 didn't jump to line 200, because the condition on line 199 was never true
200 unittest.main()