Coverage for tests/test_logging.py: 9%
196 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-15 00:10 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-15 00:10 +0000
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/>.
22import io
23import logging
24import tempfile
25import unittest
26from logging import FileHandler, StreamHandler
28import lsst.utils.logging
29from lsst.daf.butler.core.logging import (
30 ButlerLogRecord,
31 ButlerLogRecordHandler,
32 ButlerLogRecords,
33 ButlerMDC,
34 JsonLogFormatter,
35)
36from lsst.utils.logging import VERBOSE
39class LoggingTestCase(unittest.TestCase):
40 """Test we can capture log messages."""
42 def setUp(self):
43 self.handler = ButlerLogRecordHandler()
45 self.log = lsst.utils.logging.getLogger(self.id())
46 self.log.addHandler(self.handler)
48 def tearDown(self):
49 if self.handler and self.log:
50 self.log.removeHandler(self.handler)
51 ButlerMDC.restore_log_record_factory()
53 def testRecordCapture(self):
54 """Test basic log capture and serialization."""
56 self.log.setLevel(VERBOSE)
58 test_messages = (
59 (logging.INFO, "This is a log message", True),
60 (logging.WARNING, "This is a warning message", True),
61 (logging.DEBUG, "This debug message should not be stored", False),
62 (VERBOSE, "A verbose message should appear", True),
63 )
65 for level, message, _ in test_messages:
66 self.log.log(level, message)
68 expected = [info for info in test_messages if info[2]]
70 self.assertEqual(len(self.handler.records), len(expected))
72 for given, record in zip(expected, self.handler.records):
73 self.assertEqual(given[0], record.levelno)
74 self.assertEqual(given[1], record.message)
76 # Check that we can serialize the records
77 json = self.handler.records.json()
79 records = ButlerLogRecords.parse_raw(json)
80 for original_record, new_record in zip(self.handler.records, records):
81 self.assertEqual(new_record, original_record)
82 self.assertEqual(str(records), str(self.handler.records))
84 # Create stream form of serialization.
85 json_stream = "\n".join(record.json() for record in records)
87 # Also check we can autodetect the format.
88 for raw in (json, json.encode(), json_stream, json_stream.encode()):
89 records = ButlerLogRecords.from_raw(json)
90 self.assertEqual(records, self.handler.records)
92 for raw in ("", b""):
93 self.assertEqual(len(ButlerLogRecords.from_raw(raw)), 0)
94 self.assertEqual(len(ButlerLogRecords.from_stream(io.StringIO())), 0)
96 # Send bad text to the parser and it should fail (both bytes and str).
97 bad_text = "x" * 100
99 # Include short and long values to trigger different code paths
100 # in error message creation.
101 for trim in (True, False):
102 for bad in (bad_text, bad_text.encode()):
103 bad = bad[:10] if trim else bad
104 with self.assertRaises(ValueError) as cm:
105 ButlerLogRecords.from_raw(bad)
106 if not trim:
107 self.assertIn("...", str(cm.exception))
109 def testRecordsFormatting(self):
110 self.log.setLevel(logging.DEBUG)
111 self.log.debug("debug message")
112 self.log.warning("warning message")
113 self.log.critical("critical message")
114 self.log.verbose("verbose message")
116 self.assertEqual(len(self.handler.records), 4)
118 format_default = str(self.handler.records)
120 # Set the format for these records.
121 self.handler.records.set_log_format("{levelname}")
122 format_override = str(self.handler.records)
124 self.assertNotEqual(format_default, format_override)
125 self.assertEqual(format_override, "DEBUG\nWARNING\nCRITICAL\nVERBOSE")
127 # Reset the log format and it should match the original text.
128 self.handler.records.set_log_format(None)
129 self.assertEqual(str(self.handler.records), format_default)
131 def testButlerLogRecords(self):
132 """Test the list-like methods of ButlerLogRecords."""
134 self.log.setLevel(logging.INFO)
136 n_messages = 10
137 message = "Message #%d"
138 for counter in range(n_messages):
139 self.log.info(message, counter)
141 records = self.handler.records
142 self.assertEqual(len(records), n_messages)
144 # Test slicing.
145 start = 2
146 end = 6
147 subset = records[start:end]
148 self.assertIsInstance(subset, ButlerLogRecords)
149 self.assertEqual(len(subset), end - start)
150 self.assertIn(f"#{start}", subset[0].message)
152 # Reverse the collection.
153 backwards = list(reversed(records))
154 self.assertEqual(len(backwards), len(records))
155 self.assertEqual(records[0], backwards[-1])
157 # Test some of the collection manipulation methods.
158 record_0 = records[0]
159 records.reverse()
160 self.assertEqual(records[-1], record_0)
161 self.assertEqual(records.pop(), record_0)
162 records[0] = record_0
163 self.assertEqual(records[0], record_0)
164 len_records = len(records)
165 records.insert(2, record_0)
166 self.assertEqual(len(records), len_records + 1)
167 self.assertEqual(records[0], records[2])
169 # Put the subset records back onto the end of the original.
170 records.extend(subset)
171 self.assertEqual(len(records), n_messages + len(subset))
173 # Test slice for deleting
174 initial_length = len(records)
175 start_del = 1
176 end_del = 3
177 del records[start_del:end_del]
178 self.assertEqual(len(records), initial_length - (end_del - start_del))
180 records.clear()
181 self.assertEqual(len(records), 0)
183 with self.assertRaises(ValueError):
184 records.append({})
186 def testExceptionInfo(self):
187 self.log.setLevel(logging.DEBUG)
188 try:
189 raise RuntimeError("A problem has been encountered.")
190 except RuntimeError:
191 self.log.exception("Caught")
193 self.assertIn("A problem has been encountered", self.handler.records[0].exc_info)
195 self.log.warning("No exc_info")
196 self.assertIsNone(self.handler.records[-1].exc_info)
198 try:
199 raise RuntimeError("Debug exception log")
200 except RuntimeError:
201 self.log.debug("A problem", exc_info=1)
203 self.assertIn("Debug exception", self.handler.records[-1].exc_info)
205 def testMDC(self):
206 """Test that MDC information appears in messages."""
207 self.log.setLevel(logging.INFO)
209 i = 0
210 self.log.info("Message %d", i)
211 i += 1
212 self.assertEqual(self.handler.records[-1].MDC, {})
214 ButlerMDC.add_mdc_log_record_factory()
215 label = "MDC value"
216 ButlerMDC.MDC("LABEL", label)
217 self.log.info("Message %d", i)
218 self.assertEqual(self.handler.records[-1].MDC["LABEL"], label)
220 # Change the label and check that the previous record does not
221 # itself change.
222 ButlerMDC.MDC("LABEL", "dataId")
223 self.assertEqual(self.handler.records[-1].MDC["LABEL"], label)
225 # Format a record with MDC.
226 record = self.handler.records[-1]
228 # By default the MDC label should not be involved.
229 self.assertNotIn(label, str(record))
231 # But it can be included.
232 fmt = "x{MDC[LABEL]}"
233 self.assertEqual(record.format(fmt), "x" + label)
235 # But can be optional on a record that didn't set it.
236 self.assertEqual(self.handler.records[0].format(fmt), "x")
238 # Set an extra MDC entry and include all content.
239 extra = "extra"
240 ButlerMDC.MDC("EXTRA", extra)
242 i += 1
243 self.log.info("Message %d", i)
244 formatted = self.handler.records[-1].format("x{MDC} - {message}")
245 self.assertIn(f"EXTRA={extra}", formatted)
246 self.assertIn("LABEL=dataId", formatted)
247 self.assertIn(f"Message {i}", formatted)
249 # Clear the MDC and ensure that it does not continue to appear
250 # in messages.
251 ButlerMDC.MDCRemove("LABEL")
252 i += 1
253 self.log.info("Message %d", i)
254 self.assertEqual(self.handler.records[-1].format(fmt), "x")
255 self.assertEqual(self.handler.records[-1].format("{message}"), f"Message {i}")
257 # MDC context manager
258 fmt = "x{MDC[LABEL]} - {message}"
259 ButlerMDC.MDC("LABEL", "original")
260 with ButlerMDC.set_mdc({"LABEL": "test"}):
261 i += 1
262 self.log.info("Message %d", i)
263 self.assertEqual(self.handler.records[-1].format(fmt), f"xtest - Message {i}")
264 i += 1
265 self.log.info("Message %d", i)
266 self.assertEqual(self.handler.records[-1].format(fmt), f"xoriginal - Message {i}")
269class TestJsonLogging(unittest.TestCase):
270 def testJsonLogStream(self):
271 log = logging.getLogger(self.id())
272 log.setLevel(logging.INFO)
274 # Log to a stream and also to a file.
275 formatter = JsonLogFormatter()
277 stream = io.StringIO()
278 stream_handler = StreamHandler(stream)
279 stream_handler.setFormatter(formatter)
280 log.addHandler(stream_handler)
282 file = tempfile.NamedTemporaryFile(suffix=".json")
283 filename = file.name
284 file.close()
286 file_handler = FileHandler(filename)
287 file_handler.setFormatter(formatter)
288 log.addHandler(file_handler)
290 log.info("A message")
291 log.warning("A warning")
293 # Add a blank line to the stream to check the parser ignores it.
294 print(file=stream)
296 # Rewind the stream and pull messages out of it.
297 stream.seek(0)
298 records = ButlerLogRecords.from_stream(stream)
299 self.assertIsInstance(records[0], ButlerLogRecord)
300 self.assertEqual(records[0].message, "A message")
301 self.assertEqual(records[1].levelname, "WARNING")
303 # Now read from the file. Add two blank lines to test the parser
304 # will filter them out.
305 file_handler.close()
307 with open(filename, "a") as fd:
308 print(file=fd)
309 print(file=fd)
311 file_records = ButlerLogRecords.from_file(filename)
312 self.assertEqual(file_records, records)
314 # And read the file again in bytes and text.
315 for mode in ("rb", "r"):
316 with open(filename, mode) as fd:
317 file_records = ButlerLogRecords.from_stream(fd)
318 self.assertEqual(file_records, records)
319 fd.seek(0)
320 file_records = ButlerLogRecords.from_raw(fd.read())
321 self.assertEqual(file_records, records)
323 # Serialize this model to stream.
324 stream2 = io.StringIO()
325 print(records.json(), file=stream2)
326 stream2.seek(0)
327 stream_records = ButlerLogRecords.from_stream(stream2)
328 self.assertEqual(stream_records, records)
331if __name__ == "__main__": 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true
332 unittest.main()