Coverage for tests/test_log.py: 10%

355 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-15 02:24 -0700

1 

2# LSST Data Management System 

3# Copyright 2014-2017 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21 

22""" 

23This tests the logging system in a variety of ways. 

24""" 

25 

26 

27import os 

28import shutil 

29import tempfile 

30import threading 

31import unittest 

32import logging 

33import lsst.log as log 

34 

35 

36class TestLog(unittest.TestCase): 

37 

38 class StdoutCapture(object): 

39 """ 

40 Context manager to redirect stdout to a file. 

41 """ 

42 

43 def __init__(self, filename): 

44 self.stdout = None 

45 self.outputFilename = filename 

46 

47 def __enter__(self): 

48 self.stdout = os.dup(1) 

49 os.close(1) 

50 os.open(self.outputFilename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) 

51 

52 def __exit__(self, type, value, traceback): 

53 if self.stdout is not None: 

54 os.close(1) 

55 os.dup(self.stdout) 

56 os.close(self.stdout) 

57 self.stdout = None 

58 

59 def setUp(self): 

60 """Make a temporary directory and a log file in it.""" 

61 self.tempDir = tempfile.mkdtemp() 

62 self.outputFilename = os.path.join(self.tempDir, "log.out") 

63 self.stdout = None 

64 

65 def tearDown(self): 

66 """Remove the temporary directory and clean up Python forwarding.""" 

67 log.doNotUsePythonLogging() 

68 shutil.rmtree(self.tempDir) 

69 

70 def configure(self, configuration): 

71 """ 

72 Create a configuration file in the temporary directory and populate 

73 it with the provided string. 

74 """ 

75 log.configure_prop(configuration.format(self.outputFilename)) 

76 

77 def check(self, reference): 

78 """Compare the log file with the provided reference text.""" 

79 with open(self.outputFilename, 'r') as f: 

80 # strip everything up to first ] to remove timestamp and thread ID 

81 lines = [line.split(']')[-1].rstrip("\n") for line in f.readlines()] 

82 reflines = [line for line in reference.split("\n") if line != ""] 

83 self.maxDiff = None 

84 self.assertListEqual(lines, reflines) 

85 

86 def testDefaultLogger(self): 

87 """Check the default root logger name.""" 

88 self.assertEqual(log.getDefaultLogger().getName(), "") 

89 

90 def testBasic(self): 

91 """ 

92 Test basic log output with default configuration. 

93 Since the default threshold is INFO, the DEBUG or TRACE 

94 message is not emitted. 

95 """ 

96 with TestLog.StdoutCapture(self.outputFilename): 

97 log.configure() 

98 log.log(log.getDefaultLogger(), log.INFO, "This is INFO") 

99 log.info(u"This is unicode INFO") 

100 log.trace("This is TRACE") 

101 log.debug("This is DEBUG") 

102 log.warn("This is WARN") 

103 log.error("This is ERROR") 

104 log.fatal("This is FATAL") 

105 log.critical("This is CRITICAL") 

106 log.warning("Format %d %g %s", 3, 2.71828, "foo") 

107 self.check(""" 

108root INFO: This is INFO 

109root INFO: This is unicode INFO 

110root WARN: This is WARN 

111root ERROR: This is ERROR 

112root FATAL: This is FATAL 

113root FATAL: This is CRITICAL 

114root WARN: Format 3 2.71828 foo 

115""") 

116 

117 def testPattern(self): 

118 """ 

119 Test a complex pattern for log messages, including Mapped 

120 Diagnostic Context (MDC). 

121 """ 

122 with TestLog.StdoutCapture(self.outputFilename): 

123 self.configure(""" 

124log4j.rootLogger=DEBUG, CA 

125log4j.appender.CA=ConsoleAppender 

126log4j.appender.CA.layout=PatternLayout 

127log4j.appender.CA.layout.ConversionPattern=%-5p %c %C %M (%F:%L) %l - %m - %X%n 

128""") 

129 log.trace("This is TRACE") 

130 log.info("This is INFO") 

131 log.debug("This is DEBUG") 

132 

133 log.MDC("x", 3) 

134 log.MDC("y", "foo") 

135 log.MDC("z", TestLog) 

136 

137 log.trace("This is TRACE 2") 

138 log.info("This is INFO 2") 

139 log.debug("This is DEBUG 2") 

140 log.MDCRemove("z") 

141 

142 log.trace("This is TRACE 3") 

143 log.info("This is INFO 3") 

144 log.debug("This is DEBUG 3") 

145 log.MDCRemove("x") 

146 log.trace("This is TRACE 4") 

147 log.info("This is INFO 4") 

148 log.debug("This is DEBUG 4") 

149 

150 log.trace("This is TRACE 5") 

151 log.info("This is INFO 5") 

152 log.debug("This is DEBUG 5") 

153 

154 log.MDCRemove("y") 

155 

156 # Use format to make line numbers easier to change. 

157 self.check(""" 

158INFO root testPattern (test_log.py:{0[0]}) test_log.py({0[0]}) - This is INFO - {{}} 

159DEBUG root testPattern (test_log.py:{0[1]}) test_log.py({0[1]}) - This is DEBUG - {{}} 

160INFO root testPattern (test_log.py:{0[2]}) test_log.py({0[2]}) - This is INFO 2 - {{{{x,3}}{{y,foo}}{{z,<class '{1}.TestLog'>}}}} 

161DEBUG root testPattern (test_log.py:{0[3]}) test_log.py({0[3]}) - This is DEBUG 2 - {{{{x,3}}{{y,foo}}{{z,<class '{1}.TestLog'>}}}} 

162INFO root testPattern (test_log.py:{0[4]}) test_log.py({0[4]}) - This is INFO 3 - {{{{x,3}}{{y,foo}}}} 

163DEBUG root testPattern (test_log.py:{0[5]}) test_log.py({0[5]}) - This is DEBUG 3 - {{{{x,3}}{{y,foo}}}} 

164INFO root testPattern (test_log.py:{0[6]}) test_log.py({0[6]}) - This is INFO 4 - {{{{y,foo}}}} 

165DEBUG root testPattern (test_log.py:{0[7]}) test_log.py({0[7]}) - This is DEBUG 4 - {{{{y,foo}}}} 

166INFO root testPattern (test_log.py:{0[8]}) test_log.py({0[8]}) - This is INFO 5 - {{{{y,foo}}}} 

167DEBUG root testPattern (test_log.py:{0[9]}) test_log.py({0[9]}) - This is DEBUG 5 - {{{{y,foo}}}} 

168""".format([x + 130 for x in (0, 1, 8, 9, 13, 14, 17, 18, 21, 22)], __name__)) # noqa E501 line too long 

169 

170 def testMDCPutPid(self): 

171 """ 

172 Test add of PID Mapped Diagnostic Context (MDC). 

173 """ 

174 pid = os.fork() 

175 try: 

176 

177 log.MDC("PID", os.getpid()) 

178 self.configure(""" 

179log4j.rootLogger=DEBUG, CA 

180log4j.appender.CA=ConsoleAppender 

181log4j.appender.CA.layout=PatternLayout 

182log4j.appender.CA.layout.ConversionPattern=%-5p PID:%X{{PID}} %c %C %M (%F:%L) %l - %m%n 

183""") # noqa E501 line too long 

184 self.assertGreaterEqual(pid, 0, "Failed to fork") 

185 

186 msg = "This is INFO" 

187 if pid == 0: 

188 self.tempDir = tempfile.mkdtemp() 

189 self.outputFilename = os.path.join(self.tempDir, 

190 "log-child.out") 

191 msg += " in child process" 

192 elif pid > 0: 

193 child_pid, child_status = os.wait() 

194 self.assertEqual(child_status, 0, 

195 "Child returns incorrect code") 

196 msg += " in parent process" 

197 

198 with TestLog.StdoutCapture(self.outputFilename): 

199 log.info(msg) 

200 line = 199 # line number for previous line 

201 finally: 

202 log.MDCRemove("PID") 

203 

204 # Use format to make line numbers easier to change. 

205 self.check(""" 

206INFO PID:{1} root testMDCPutPid (test_log.py:{0}) test_log.py({0}) - {2} 

207""".format(line, os.getpid(), msg)) 

208 

209 # don't pass other tests in child process 

210 if pid == 0: 

211 os._exit(0) 

212 

213 def testFileAppender(self): 

214 """Test configuring logging to go to a file.""" 

215 self.configure(""" 

216log4j.rootLogger=DEBUG, FA 

217log4j.appender.FA=FileAppender 

218log4j.appender.FA.file={0} 

219log4j.appender.FA.layout=SimpleLayout 

220""") 

221 log.MDC("x", 3) 

222 log.trace("This is TRACE") 

223 log.info("This is INFO") 

224 log.debug("This is DEBUG") 

225 log.MDCRemove("x") 

226 

227 self.check(""" 

228INFO - This is INFO 

229DEBUG - This is DEBUG 

230""") 

231 

232 def testPythonLogging(self): 

233 """Test logging through the Python logging interface.""" 

234 with TestLog.StdoutCapture(self.outputFilename): 

235 lgr = logging.getLogger() 

236 lgr.setLevel(logging.INFO) 

237 log.configure() 

238 with self.assertLogs(level="INFO") as cm: 

239 # Force the lsst.log handler to be applied as well as the 

240 # unittest log handler 

241 lgr.addHandler(log.LogHandler()) 

242 lgr.info("This is INFO") 

243 lgr.debug("This is DEBUG") 

244 lgr.warning("This is %s", "WARNING") 

245 # message can be arbitrary Python object 

246 lgr.info(((1, 2), (3, 4))) 

247 lgr.info({1: 2}) 

248 

249 # Confirm that Python logging also worked 

250 self.assertEqual(len(cm.output), 4, f"Got output: {cm.output}") 

251 logging.shutdown() 

252 

253 self.check(""" 

254root INFO: This is INFO 

255root WARN: This is WARNING 

256root INFO: ((1, 2), (3, 4)) 

257root INFO: {1: 2} 

258""") 

259 

260 def testMdcInit(self): 

261 

262 expected_msg = \ 

263 "INFO - main thread {{MDC_INIT,OK}}\n" + \ 

264 "INFO - thread 1 {{MDC_INIT,OK}}\n" + \ 

265 "INFO - thread 2 {{MDC_INIT,OK}}\n" 

266 

267 with TestLog.StdoutCapture(self.outputFilename): 

268 

269 self.configure(""" 

270log4j.rootLogger=DEBUG, CA 

271log4j.appender.CA=ConsoleAppender 

272log4j.appender.CA.layout=PatternLayout 

273log4j.appender.CA.layout.ConversionPattern=%-5p - %m %X%n 

274""") 

275 

276 def fun(): 

277 log.MDC("MDC_INIT", "OK") 

278 log.MDCRegisterInit(fun) 

279 

280 log.info("main thread") 

281 

282 thread = threading.Thread(target=lambda: log.info("thread 1")) 

283 thread.start() 

284 thread.join() 

285 

286 thread = threading.Thread(target=lambda: log.info("thread 2")) 

287 thread.start() 

288 thread.join() 

289 

290 self.check(expected_msg) 

291 

292 log.MDCRemove("MDC_INIT") 

293 

294 def testMdcUpdate(self): 

295 """Test for overwriting MDC. 

296 """ 

297 

298 expected_msg = \ 

299 "INFO - Message one {}\n" \ 

300 "INFO - Message two {{LABEL,123456}}\n" \ 

301 "INFO - Message three {{LABEL,654321}}\n" \ 

302 "INFO - Message four {}\n" 

303 

304 with TestLog.StdoutCapture(self.outputFilename): 

305 

306 self.configure(""" 

307log4j.rootLogger=DEBUG, CA 

308log4j.appender.CA=ConsoleAppender 

309log4j.appender.CA.layout=PatternLayout 

310log4j.appender.CA.layout.ConversionPattern=%-5p - %m %X%n 

311""") 

312 

313 log.info("Message one") 

314 

315 log.MDC("LABEL", "123456") 

316 log.info("Message two") 

317 

318 log.MDC("LABEL", "654321") 

319 log.info("Message three") 

320 

321 log.MDCRemove("LABEL") 

322 log.info("Message four") 

323 

324 self.check(expected_msg) 

325 

326 def testLwpID(self): 

327 """Test log.lwpID() method.""" 

328 lwp1 = log.lwpID() 

329 lwp2 = log.lwpID() 

330 

331 self.assertEqual(lwp1, lwp2) 

332 

333 def testLogger(self): 

334 """ 

335 Test log object. 

336 """ 

337 with TestLog.StdoutCapture(self.outputFilename): 

338 log.configure() 

339 logger = log.Log.getLogger("b") 

340 self.assertEqual(logger.getName(), "b") 

341 self.assertEqual(logger.name, logger.getName()) 

342 logger.trace("This is TRACE") 

343 logger.info("This is INFO") 

344 logger.debug("This is DEBUG") 

345 logger.warn("This is WARN") 

346 logger.error("This is ERROR") 

347 logger.fatal("This is FATAL") 

348 logger.warn("Format %d %g %s", 3, 2.71828, "foo") 

349 self.check(""" 

350b INFO: This is INFO 

351b WARN: This is WARN 

352b ERROR: This is ERROR 

353b FATAL: This is FATAL 

354b WARN: Format 3 2.71828 foo 

355""") 

356 

357 def testLoggerLevel(self): 

358 """ 

359 Test levels of Log objects 

360 """ 

361 with TestLog.StdoutCapture(self.outputFilename): 

362 self.configure(""" 

363log4j.rootLogger=TRACE, CA 

364log4j.appender.CA=ConsoleAppender 

365log4j.appender.CA.layout=PatternLayout 

366log4j.appender.CA.layout.ConversionPattern=%-5p %c (%F)- %m%n 

367""") 

368 self.assertEqual(log.Log.getLevel(log.Log.getDefaultLogger()), 

369 log.TRACE) 

370 self.assertEqual(log.Log.getDefaultLogger().getEffectiveLevel(), log.TRACE) 

371 logger = log.Log.getLogger("a.b") 

372 self.assertEqual(logger.getName(), "a.b") 

373 self.assertEqual(logger.getEffectiveLevel(), log.TRACE) 

374 self.assertEqual(log.getEffectiveLevel(logger.name), log.TRACE) 

375 logger.trace("This is TRACE") 

376 logger.setLevel(log.INFO) 

377 self.assertEqual(logger.getLevel(), log.INFO) 

378 self.assertEqual(logger.getEffectiveLevel(), log.INFO) 

379 self.assertEqual(logger.level, logger.getLevel()) 

380 self.assertEqual(log.Log.getLevel(logger), log.INFO) 

381 logger.debug("This is DEBUG") 

382 logger.info("This is INFO") 

383 logger.fatal("Format %d %g %s", 3, 2.71828, "foo") 

384 

385 logger = log.Log.getLogger("a.b.c") 

386 self.assertEqual(logger.getName(), "a.b.c") 

387 logger.trace("This is TRACE") 

388 logger.debug("This is DEBUG") 

389 logger.warn("This is WARN") 

390 logger.error("This is ERROR") 

391 logger.fatal("This is FATAL") 

392 logger.info("Format %d %g %s", 3, 2.71828, "foo") 

393 self.check(""" 

394TRACE a.b (test_log.py)- This is TRACE 

395INFO a.b (test_log.py)- This is INFO 

396FATAL a.b (test_log.py)- Format 3 2.71828 foo 

397WARN a.b.c (test_log.py)- This is WARN 

398ERROR a.b.c (test_log.py)- This is ERROR 

399FATAL a.b.c (test_log.py)- This is FATAL 

400INFO a.b.c (test_log.py)- Format 3 2.71828 foo 

401""") 

402 

403 def testMsgWithPercentS(self): 

404 """Test logging messages containing %s (DM-7509) 

405 """ 

406 with TestLog.StdoutCapture(self.outputFilename): 

407 log.configure() 

408 logger = log.Log() 

409 logger.info("INFO with %s") 

410 logger.trace("TRACE with %s") 

411 logger.debug("DEBUG with %s") 

412 logger.warn("WARN with %s") 

413 logger.error("ERROR with %s") 

414 logger.fatal("FATAL with %s") 

415 logger.logMsg(log.DEBUG, "foo", "bar", 5, "DEBUG with %s") 

416 self.check(""" 

417root INFO: INFO with %s 

418root WARN: WARN with %s 

419root ERROR: ERROR with %s 

420root FATAL: FATAL with %s 

421root DEBUG: DEBUG with %s 

422""") 

423 

424 def testForwardToPython(self): 

425 """Test that `lsst.log` log messages can be forwarded to `logging`.""" 

426 log.configure() 

427 

428 # Without forwarding we only get python logger messages captured 

429 with self.assertLogs(level="WARNING") as cm: 

430 log.warn("lsst.log warning message that will not be forwarded to Python") 

431 logging.warning("Python logging message that will be captured") 

432 self.assertEqual(len(cm.output), 1) 

433 

434 log.usePythonLogging() 

435 

436 # With forwarding we get 2 logging messages captured 

437 with self.assertLogs(level="WARNING") as cm: 

438 log.warn("This is a warning from lsst log meant for python logging") 

439 logging.warning("Python warning log message to be captured") 

440 self.assertEqual(len(cm.output), 2) 

441 

442 loggername = "newlogger" 

443 log2 = log.Log.getLogger(loggername) 

444 with self.assertLogs(level="INFO", logger=loggername): 

445 log2.info("Info message to non-root lsst logger") 

446 

447 # Check that debug and info are working properly 

448 # This test should return a single log message 

449 with self.assertLogs(level="INFO", logger=loggername) as cm: 

450 log2.info("Second INFO message to non-root lsst logger") 

451 log.debug("Debug message to root lsst logger") 

452 

453 self.assertEqual(len(cm.output), 1, f"Got output: {cm.output}") 

454 

455 logging.shutdown() 

456 

457 def testLogLoop(self): 

458 """Test that Python log forwarding works even if Python logging has 

459 been forwarded to lsst.log""" 

460 

461 log.configure() 

462 

463 # Note that assertLogs causes a specialists Python logging handler 

464 # to be added. 

465 

466 # Set up some Python loggers 

467 loggername = "testLogLoop" 

468 lgr = logging.getLogger(loggername) 

469 lgr.setLevel(logging.INFO) 

470 rootlgr = logging.getLogger() 

471 rootlgr.setLevel(logging.INFO) 

472 

473 # Declare that we are using the Python logger and that this will 

474 # not cause a log loop if we also are forwarding Python logging to 

475 # lsst.log 

476 log.usePythonLogging() 

477 

478 # Ensure that we can log both in lsst.log and Python 

479 rootlgr.addHandler(log.LogHandler()) 

480 

481 # All three of these messages go through LogHandler 

482 # The first two because they have the handler added explicitly, the 

483 # the final one because the lsst.log logger is forwarded to the 

484 # ROOT Python logger which has the LogHandler registered. 

485 

486 with open(self.outputFilename, "w") as fd: 

487 # Adding a StreamHandler will cause the LogHandler to no-op 

488 streamHandler = logging.StreamHandler(stream=fd) 

489 rootlgr.addHandler(streamHandler) 

490 

491 # Do not use assertLogs since that messes with handlers 

492 lgr.info("INFO message: Python child logger, lsst.log.LogHandler + PythonLogging") 

493 rootlgr.info("INFO message: Python root logger, lsst.log.logHandler + PythonLogging") 

494 

495 # This will use a ROOT python logger which has a LogHandler attached 

496 log.info("INFO message: lsst.log root logger, PythonLogging") 

497 

498 rootlgr.removeHandler(streamHandler) 

499 

500 self.check(""" 

501INFO message: Python child logger, lsst.log.LogHandler + PythonLogging 

502INFO message: Python root logger, lsst.log.logHandler + PythonLogging 

503INFO message: lsst.log root logger, PythonLogging""") 

504 

505 with open(self.outputFilename, "w") as fd: 

506 # Adding a StreamHandler will cause the LogHandler to no-op 

507 streamHandler = logging.StreamHandler(stream=fd) 

508 rootlgr.addHandler(streamHandler) 

509 

510 # Do not use assertLogs since that messes with handlers 

511 lgr.info("INFO message: Python child logger, lsst.log.LogHandler + PythonLogging") 

512 rootlgr.info("INFO message: Python root logger, lsst.log.logHandler + PythonLogging") 

513 

514 # This will use a ROOT python logger which has a LogHandler attached 

515 log.info("INFO message: lsst.log root logger, PythonLogging") 

516 

517 rootlgr.removeHandler(streamHandler) 

518 

519 self.check(""" 

520INFO message: Python child logger, lsst.log.LogHandler + PythonLogging 

521INFO message: Python root logger, lsst.log.logHandler + PythonLogging 

522INFO message: lsst.log root logger, PythonLogging""") 

523 

524 with self.assertLogs(level="INFO") as cm: 

525 rootlgr.info("Python log message forward to lsst.log") 

526 log.info("lsst.log message forwarded to Python") 

527 

528 self.assertEqual(len(cm.output), 2, f"Got output: {cm.output}") 

529 

530 logging.shutdown() 

531 

532 def testForwardToPythonContextManager(self): 

533 """Test that `lsst.log` log messages can be forwarded to `logging` 

534 using context manager""" 

535 log.configure() 

536 

537 # Without forwarding we only get python logger messages captured 

538 with self.assertLogs(level="WARNING") as cm: 

539 log.warning("lsst.log: not forwarded") 

540 logging.warning("Python logging: captured") 

541 self.assertEqual(len(cm.output), 1) 

542 

543 # Temporarily turn on forwarding 

544 with log.UsePythonLogging(): 

545 with self.assertLogs(level="WARNING") as cm: 

546 log.warn("lsst.log: forwarded") 

547 logging.warning("Python logging: also captured") 

548 self.assertEqual(len(cm.output), 2) 

549 

550 # Verify that forwarding is disabled 

551 self.assertFalse(log.Log.UsePythonLogging) 

552 

553 def testForwardToPythonAppender(self): 

554 """Test that `log4cxx` appender forwards it all to logging""" 

555 self.configure(""" 

556log4j.rootLogger=DEBUG, PyLog 

557log4j.appender.PyLog = PyLogAppender 

558""") 

559 with self.assertLogs(level="WARNING") as cm: 

560 log.warn("lsst.log: forwarded") 

561 logging.warning("Python logging: also captured") 

562 self.assertEqual(len(cm.output), 2) 

563 

564 # check that MDC is stored in LogRecord 

565 log.MDC("LABEL", "some.task") 

566 with self.assertLogs(level="WARNING") as cm: 

567 log.warn("lsst.log: forwarded") 

568 log.MDCRemove("LABEL") 

569 self.assertEqual(len(cm.records), 1) 

570 self.assertEqual(cm.records[0].MDC, {"LABEL": "some.task"}) 

571 self.assertEqual(cm.records[0].msg, "lsst.log: forwarded") 

572 

573 def testForwardToPythonAppenderWithMDC(self): 

574 """Test that `log4cxx` appender forwards it all to logging and modifies 

575 message with MDC info""" 

576 self.configure(""" 

577log4j.rootLogger=DEBUG, PyLog 

578log4j.appender.PyLog = PyLogAppender 

579log4j.appender.PyLog.MessagePattern = %m (LABEL=%X{{LABEL}}) 

580""") 

581 log.MDC("LABEL", "some.task") 

582 with self.assertLogs(level="WARNING") as cm: 

583 log.warn("lsst.log: forwarded") 

584 log.MDCRemove("LABEL") 

585 self.assertEqual(len(cm.records), 1) 

586 self.assertEqual(cm.records[0].MDC, {"LABEL": "some.task"}) 

587 self.assertEqual(cm.records[0].msg, "lsst.log: forwarded (LABEL=some.task)") 

588 

589 def testForwardToPythonAppenderFormatMDC(self): 

590 """Test that we can format `log4cxx` MDC on Python side""" 

591 

592 # remember old record factory 

593 old_factory = logging.getLogRecordFactory() 

594 

595 # configure things using convenience method 

596 log.configure_pylog_MDC("INFO") 

597 

598 with self.assertLogs(level="WARNING") as cm: 

599 log.warn("lsst.log: forwarded 1") 

600 log.MDC("LABEL", "task1") 

601 log.warn("lsst.log: forwarded 2") 

602 log.MDC("LABEL-X", "task2") 

603 log.warn("lsst.log: forwarded 3") 

604 logging.warning("Python logging: also captured") 

605 log.MDCRemove("LABEL") 

606 log.MDCRemove("LABEL-X") 

607 self.assertEqual(len(cm.records), 4) 

608 

609 # restore factory 

610 logging.setLogRecordFactory(old_factory) 

611 

612 # %-style formatting, only works on whole MDC 

613 formatter = logging.Formatter(fmt="%(levelname)s:%(name)s:%(message)s", style="%") 

614 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1") 

615 formatter = logging.Formatter(fmt="%(levelname)s:%(name)s:%(message)s:%(MDC)s", style="%") 

616 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1:{}") 

617 self.assertEqual(formatter.format(cm.records[1]), 

618 "WARNING:root:lsst.log: forwarded 2:{LABEL=task1}") 

619 self.assertEqual(formatter.format(cm.records[2]), 

620 "WARNING:root:lsst.log: forwarded 3:{LABEL=task1, LABEL-X=task2}") 

621 self.assertEqual(formatter.format(cm.records[3]), "WARNING:root:Python logging: also captured:{}") 

622 

623 # format-style formatting, without MDC first 

624 formatter = logging.Formatter(fmt="{levelname}:{name}:{message}", style="{") 

625 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1") 

626 

627 # format-style formatting, with full MDC 

628 formatter = logging.Formatter(fmt="{levelname}:{name}:{message}:{MDC}", style="{") 

629 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1:{}") 

630 self.assertEqual(formatter.format(cm.records[1]), 

631 "WARNING:root:lsst.log: forwarded 2:{LABEL=task1}") 

632 self.assertEqual(formatter.format(cm.records[2]), 

633 "WARNING:root:lsst.log: forwarded 3:{LABEL=task1, LABEL-X=task2}") 

634 self.assertEqual(formatter.format(cm.records[3]), "WARNING:root:Python logging: also captured:{}") 

635 

636 # format-style, using index access to MDC items, works for almost any 

637 # item names 

638 formatter = logging.Formatter(fmt="{levelname}:{name}:{message}:{MDC[LABEL-X]}", style="{") 

639 self.assertEqual(formatter.format(cm.records[0]), "WARNING:root:lsst.log: forwarded 1:") 

640 self.assertEqual(formatter.format(cm.records[1]), "WARNING:root:lsst.log: forwarded 2:") 

641 self.assertEqual(formatter.format(cm.records[2]), 

642 "WARNING:root:lsst.log: forwarded 3:task2") 

643 self.assertEqual(formatter.format(cm.records[3]), "WARNING:root:Python logging: also captured:") 

644 

645 def testLevelTranslator(self): 

646 """Test LevelTranslator class 

647 """ 

648 # correspondence between levels, logging has no TRACE but we accept 

649 # small integer in its place 

650 levelMap = ((log.TRACE, 5), 

651 (log.DEBUG, logging.DEBUG), 

652 (log.INFO, logging.INFO), 

653 (log.WARN, logging.WARNING), 

654 (log.WARNING, logging.WARNING), 

655 (log.ERROR, logging.ERROR), 

656 (log.CRITICAL, logging.CRITICAL), 

657 (log.FATAL, logging.FATAL)) 

658 for logLevel, loggingLevel in levelMap: 

659 self.assertEqual(log.LevelTranslator.lsstLog2logging(logLevel), loggingLevel) 

660 self.assertEqual(log.LevelTranslator.logging2lsstLog(loggingLevel), logLevel) 

661 

662 def testLevelName(self): 

663 self.assertEqual(log.getLevelName(log.INFO), "INFO") 

664 self.assertEqual(log.getLevelName(3253), "Level 3253") 

665 

666 def testRepr(self): 

667 logger = log.getLogger("a.b") 

668 logger.setLevel(log.DEBUG) 

669 self.assertEqual(repr(logger), "<lsst.log.Log 'a.b' (DEBUG)>") 

670 

671 def testChildLogger(self): 

672 """Check the getChild logger method.""" 

673 logger = log.getDefaultLogger() 

674 self.assertEqual(logger.getName(), "") 

675 logger1 = logger.getChild("child1") 

676 self.assertEqual(logger1.getName(), "child1") 

677 logger2 = logger1.getChild("child2") 

678 self.assertEqual(logger2.getName(), "child1.child2") 

679 logger2a = logger1.getChild(".child2") 

680 self.assertEqual(logger2a.getName(), "child1.child2") 

681 logger3 = logger2.getChild(" .. child3") 

682 self.assertEqual(logger3.getName(), "child1.child2.child3") 

683 logger3a = logger1.getChild("child2.child3") 

684 self.assertEqual(logger3a.getName(), "child1.child2.child3") 

685 self.assertEqual(logger2.parent.name, logger1.name) 

686 self.assertEqual(logger1.parent.name, logger.name) 

687 self.assertIsNone(logger.parent) 

688 

689 

690if __name__ == "__main__": 690 ↛ 691line 690 didn't jump to line 691, because the condition on line 690 was never true

691 unittest.main()