logging.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. """ Access and control log capturing. """
  2. from __future__ import absolute_import, division, print_function
  3. import logging
  4. from contextlib import closing, contextmanager
  5. import re
  6. import six
  7. from _pytest.compat import dummy_context_manager
  8. from _pytest.config import create_terminal_writer
  9. import pytest
  10. import py
  11. DEFAULT_LOG_FORMAT = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s"
  12. DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
  13. class ColoredLevelFormatter(logging.Formatter):
  14. """
  15. Colorize the %(levelname)..s part of the log format passed to __init__.
  16. """
  17. LOGLEVEL_COLOROPTS = {
  18. logging.CRITICAL: {"red"},
  19. logging.ERROR: {"red", "bold"},
  20. logging.WARNING: {"yellow"},
  21. logging.WARN: {"yellow"},
  22. logging.INFO: {"green"},
  23. logging.DEBUG: {"purple"},
  24. logging.NOTSET: set(),
  25. }
  26. LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-]?\d*s)")
  27. def __init__(self, terminalwriter, *args, **kwargs):
  28. super(ColoredLevelFormatter, self).__init__(*args, **kwargs)
  29. if six.PY2:
  30. self._original_fmt = self._fmt
  31. else:
  32. self._original_fmt = self._style._fmt
  33. self._level_to_fmt_mapping = {}
  34. levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
  35. if not levelname_fmt_match:
  36. return
  37. levelname_fmt = levelname_fmt_match.group()
  38. for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
  39. formatted_levelname = levelname_fmt % {
  40. "levelname": logging.getLevelName(level)
  41. }
  42. # add ANSI escape sequences around the formatted levelname
  43. color_kwargs = {name: True for name in color_opts}
  44. colorized_formatted_levelname = terminalwriter.markup(
  45. formatted_levelname, **color_kwargs
  46. )
  47. self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
  48. colorized_formatted_levelname, self._fmt
  49. )
  50. def format(self, record):
  51. fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt)
  52. if six.PY2:
  53. self._fmt = fmt
  54. else:
  55. self._style._fmt = fmt
  56. return super(ColoredLevelFormatter, self).format(record)
  57. def get_option_ini(config, *names):
  58. for name in names:
  59. ret = config.getoption(name) # 'default' arg won't work as expected
  60. if ret is None:
  61. ret = config.getini(name)
  62. if ret:
  63. return ret
  64. def pytest_addoption(parser):
  65. """Add options to control log capturing."""
  66. group = parser.getgroup("logging")
  67. def add_option_ini(option, dest, default=None, type=None, **kwargs):
  68. parser.addini(
  69. dest, default=default, type=type, help="default value for " + option
  70. )
  71. group.addoption(option, dest=dest, **kwargs)
  72. add_option_ini(
  73. "--no-print-logs",
  74. dest="log_print",
  75. action="store_const",
  76. const=False,
  77. default=True,
  78. type="bool",
  79. help="disable printing caught logs on failed tests.",
  80. )
  81. add_option_ini(
  82. "--log-level",
  83. dest="log_level",
  84. default=None,
  85. help="logging level used by the logging module",
  86. )
  87. add_option_ini(
  88. "--log-format",
  89. dest="log_format",
  90. default=DEFAULT_LOG_FORMAT,
  91. help="log format as used by the logging module.",
  92. )
  93. add_option_ini(
  94. "--log-date-format",
  95. dest="log_date_format",
  96. default=DEFAULT_LOG_DATE_FORMAT,
  97. help="log date format as used by the logging module.",
  98. )
  99. parser.addini(
  100. "log_cli",
  101. default=False,
  102. type="bool",
  103. help='enable log display during test run (also known as "live logging").',
  104. )
  105. add_option_ini(
  106. "--log-cli-level", dest="log_cli_level", default=None, help="cli logging level."
  107. )
  108. add_option_ini(
  109. "--log-cli-format",
  110. dest="log_cli_format",
  111. default=None,
  112. help="log format as used by the logging module.",
  113. )
  114. add_option_ini(
  115. "--log-cli-date-format",
  116. dest="log_cli_date_format",
  117. default=None,
  118. help="log date format as used by the logging module.",
  119. )
  120. add_option_ini(
  121. "--log-file",
  122. dest="log_file",
  123. default=None,
  124. help="path to a file when logging will be written to.",
  125. )
  126. add_option_ini(
  127. "--log-file-level",
  128. dest="log_file_level",
  129. default=None,
  130. help="log file logging level.",
  131. )
  132. add_option_ini(
  133. "--log-file-format",
  134. dest="log_file_format",
  135. default=DEFAULT_LOG_FORMAT,
  136. help="log format as used by the logging module.",
  137. )
  138. add_option_ini(
  139. "--log-file-date-format",
  140. dest="log_file_date_format",
  141. default=DEFAULT_LOG_DATE_FORMAT,
  142. help="log date format as used by the logging module.",
  143. )
  144. @contextmanager
  145. def catching_logs(handler, formatter=None, level=None):
  146. """Context manager that prepares the whole logging machinery properly."""
  147. root_logger = logging.getLogger()
  148. if formatter is not None:
  149. handler.setFormatter(formatter)
  150. if level is not None:
  151. handler.setLevel(level)
  152. # Adding the same handler twice would confuse logging system.
  153. # Just don't do that.
  154. add_new_handler = handler not in root_logger.handlers
  155. if add_new_handler:
  156. root_logger.addHandler(handler)
  157. if level is not None:
  158. orig_level = root_logger.level
  159. root_logger.setLevel(min(orig_level, level))
  160. try:
  161. yield handler
  162. finally:
  163. if level is not None:
  164. root_logger.setLevel(orig_level)
  165. if add_new_handler:
  166. root_logger.removeHandler(handler)
  167. class LogCaptureHandler(logging.StreamHandler):
  168. """A logging handler that stores log records and the log text."""
  169. def __init__(self):
  170. """Creates a new log handler."""
  171. logging.StreamHandler.__init__(self, py.io.TextIO())
  172. self.records = []
  173. def emit(self, record):
  174. """Keep the log records in a list in addition to the log text."""
  175. self.records.append(record)
  176. logging.StreamHandler.emit(self, record)
  177. def reset(self):
  178. self.records = []
  179. self.stream = py.io.TextIO()
  180. class LogCaptureFixture(object):
  181. """Provides access and control of log capturing."""
  182. def __init__(self, item):
  183. """Creates a new funcarg."""
  184. self._item = item
  185. self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level
  186. def _finalize(self):
  187. """Finalizes the fixture.
  188. This restores the log levels changed by :meth:`set_level`.
  189. """
  190. # restore log levels
  191. for logger_name, level in self._initial_log_levels.items():
  192. logger = logging.getLogger(logger_name)
  193. logger.setLevel(level)
  194. @property
  195. def handler(self):
  196. """
  197. :rtype: LogCaptureHandler
  198. """
  199. return self._item.catch_log_handler
  200. def get_records(self, when):
  201. """
  202. Get the logging records for one of the possible test phases.
  203. :param str when:
  204. Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
  205. :rtype: List[logging.LogRecord]
  206. :return: the list of captured records at the given stage
  207. .. versionadded:: 3.4
  208. """
  209. handler = self._item.catch_log_handlers.get(when)
  210. if handler:
  211. return handler.records
  212. else:
  213. return []
  214. @property
  215. def text(self):
  216. """Returns the log text."""
  217. return self.handler.stream.getvalue()
  218. @property
  219. def records(self):
  220. """Returns the list of log records."""
  221. return self.handler.records
  222. @property
  223. def record_tuples(self):
  224. """Returns a list of a striped down version of log records intended
  225. for use in assertion comparison.
  226. The format of the tuple is:
  227. (logger_name, log_level, message)
  228. """
  229. return [(r.name, r.levelno, r.getMessage()) for r in self.records]
  230. @property
  231. def messages(self):
  232. """Returns a list of format-interpolated log messages.
  233. Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
  234. are all interpolated.
  235. Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
  236. levels, timestamps, etc, making exact comparisions more reliable.
  237. Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
  238. to the logging functions) is not included, as this is added by the formatter in the handler.
  239. .. versionadded:: 3.7
  240. """
  241. return [r.getMessage() for r in self.records]
  242. def clear(self):
  243. """Reset the list of log records and the captured log text."""
  244. self.handler.reset()
  245. def set_level(self, level, logger=None):
  246. """Sets the level for capturing of logs. The level will be restored to its previous value at the end of
  247. the test.
  248. :param int level: the logger to level.
  249. :param str logger: the logger to update the level. If not given, the root logger level is updated.
  250. .. versionchanged:: 3.4
  251. The levels of the loggers changed by this function will be restored to their initial values at the
  252. end of the test.
  253. """
  254. logger_name = logger
  255. logger = logging.getLogger(logger_name)
  256. # save the original log-level to restore it during teardown
  257. self._initial_log_levels.setdefault(logger_name, logger.level)
  258. logger.setLevel(level)
  259. @contextmanager
  260. def at_level(self, level, logger=None):
  261. """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
  262. level is restored to its original value.
  263. :param int level: the logger to level.
  264. :param str logger: the logger to update the level. If not given, the root logger level is updated.
  265. """
  266. logger = logging.getLogger(logger)
  267. orig_level = logger.level
  268. logger.setLevel(level)
  269. try:
  270. yield
  271. finally:
  272. logger.setLevel(orig_level)
  273. @pytest.fixture
  274. def caplog(request):
  275. """Access and control log capturing.
  276. Captured logs are available through the following methods::
  277. * caplog.text -> string containing formatted log output
  278. * caplog.records -> list of logging.LogRecord instances
  279. * caplog.record_tuples -> list of (logger_name, level, message) tuples
  280. * caplog.clear() -> clear captured records and formatted log output string
  281. """
  282. result = LogCaptureFixture(request.node)
  283. yield result
  284. result._finalize()
  285. def get_actual_log_level(config, *setting_names):
  286. """Return the actual logging level."""
  287. for setting_name in setting_names:
  288. log_level = config.getoption(setting_name)
  289. if log_level is None:
  290. log_level = config.getini(setting_name)
  291. if log_level:
  292. break
  293. else:
  294. return
  295. if isinstance(log_level, six.string_types):
  296. log_level = log_level.upper()
  297. try:
  298. return int(getattr(logging, log_level, log_level))
  299. except ValueError:
  300. # Python logging does not recognise this as a logging level
  301. raise pytest.UsageError(
  302. "'{}' is not recognized as a logging level name for "
  303. "'{}'. Please consider passing the "
  304. "logging level num instead.".format(log_level, setting_name)
  305. )
  306. def pytest_configure(config):
  307. config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
  308. class LoggingPlugin(object):
  309. """Attaches to the logging module and captures log messages for each test.
  310. """
  311. def __init__(self, config):
  312. """Creates a new plugin to capture log messages.
  313. The formatter can be safely shared across all handlers so
  314. create a single one for the entire test session here.
  315. """
  316. self._config = config
  317. # enable verbose output automatically if live logging is enabled
  318. if self._log_cli_enabled() and not config.getoption("verbose"):
  319. # sanity check: terminal reporter should not have been loaded at this point
  320. assert self._config.pluginmanager.get_plugin("terminalreporter") is None
  321. config.option.verbose = 1
  322. self.print_logs = get_option_ini(config, "log_print")
  323. self.formatter = logging.Formatter(
  324. get_option_ini(config, "log_format"),
  325. get_option_ini(config, "log_date_format"),
  326. )
  327. self.log_level = get_actual_log_level(config, "log_level")
  328. log_file = get_option_ini(config, "log_file")
  329. if log_file:
  330. self.log_file_level = get_actual_log_level(config, "log_file_level")
  331. log_file_format = get_option_ini(config, "log_file_format", "log_format")
  332. log_file_date_format = get_option_ini(
  333. config, "log_file_date_format", "log_date_format"
  334. )
  335. # Each pytest runtests session will write to a clean logfile
  336. self.log_file_handler = logging.FileHandler(
  337. log_file, mode="w", encoding="UTF-8"
  338. )
  339. log_file_formatter = logging.Formatter(
  340. log_file_format, datefmt=log_file_date_format
  341. )
  342. self.log_file_handler.setFormatter(log_file_formatter)
  343. else:
  344. self.log_file_handler = None
  345. # initialized during pytest_runtestloop
  346. self.log_cli_handler = None
  347. def _log_cli_enabled(self):
  348. """Return True if log_cli should be considered enabled, either explicitly
  349. or because --log-cli-level was given in the command-line.
  350. """
  351. return self._config.getoption(
  352. "--log-cli-level"
  353. ) is not None or self._config.getini("log_cli")
  354. @contextmanager
  355. def _runtest_for(self, item, when):
  356. """Implements the internals of pytest_runtest_xxx() hook."""
  357. with catching_logs(
  358. LogCaptureHandler(), formatter=self.formatter, level=self.log_level
  359. ) as log_handler:
  360. if self.log_cli_handler:
  361. self.log_cli_handler.set_when(when)
  362. if item is None:
  363. yield # run the test
  364. return
  365. if not hasattr(item, "catch_log_handlers"):
  366. item.catch_log_handlers = {}
  367. item.catch_log_handlers[when] = log_handler
  368. item.catch_log_handler = log_handler
  369. try:
  370. yield # run test
  371. finally:
  372. del item.catch_log_handler
  373. if when == "teardown":
  374. del item.catch_log_handlers
  375. if self.print_logs:
  376. # Add a captured log section to the report.
  377. log = log_handler.stream.getvalue().strip()
  378. item.add_report_section(when, "log", log)
  379. @pytest.hookimpl(hookwrapper=True)
  380. def pytest_runtest_setup(self, item):
  381. with self._runtest_for(item, "setup"):
  382. yield
  383. @pytest.hookimpl(hookwrapper=True)
  384. def pytest_runtest_call(self, item):
  385. with self._runtest_for(item, "call"):
  386. yield
  387. @pytest.hookimpl(hookwrapper=True)
  388. def pytest_runtest_teardown(self, item):
  389. with self._runtest_for(item, "teardown"):
  390. yield
  391. @pytest.hookimpl(hookwrapper=True)
  392. def pytest_runtest_logstart(self):
  393. if self.log_cli_handler:
  394. self.log_cli_handler.reset()
  395. with self._runtest_for(None, "start"):
  396. yield
  397. @pytest.hookimpl(hookwrapper=True)
  398. def pytest_runtest_logfinish(self):
  399. with self._runtest_for(None, "finish"):
  400. yield
  401. @pytest.hookimpl(hookwrapper=True)
  402. def pytest_runtestloop(self, session):
  403. """Runs all collected test items."""
  404. self._setup_cli_logging()
  405. with self.live_logs_context:
  406. if self.log_file_handler is not None:
  407. with closing(self.log_file_handler):
  408. with catching_logs(
  409. self.log_file_handler, level=self.log_file_level
  410. ):
  411. yield # run all the tests
  412. else:
  413. yield # run all the tests
  414. def _setup_cli_logging(self):
  415. """Sets up the handler and logger for the Live Logs feature, if enabled.
  416. This must be done right before starting the loop so we can access the terminal reporter plugin.
  417. """
  418. terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
  419. if self._log_cli_enabled() and terminal_reporter is not None:
  420. capture_manager = self._config.pluginmanager.get_plugin("capturemanager")
  421. log_cli_handler = _LiveLoggingStreamHandler(
  422. terminal_reporter, capture_manager
  423. )
  424. log_cli_format = get_option_ini(
  425. self._config, "log_cli_format", "log_format"
  426. )
  427. log_cli_date_format = get_option_ini(
  428. self._config, "log_cli_date_format", "log_date_format"
  429. )
  430. if (
  431. self._config.option.color != "no"
  432. and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format)
  433. ):
  434. log_cli_formatter = ColoredLevelFormatter(
  435. create_terminal_writer(self._config),
  436. log_cli_format,
  437. datefmt=log_cli_date_format,
  438. )
  439. else:
  440. log_cli_formatter = logging.Formatter(
  441. log_cli_format, datefmt=log_cli_date_format
  442. )
  443. log_cli_level = get_actual_log_level(
  444. self._config, "log_cli_level", "log_level"
  445. )
  446. self.log_cli_handler = log_cli_handler
  447. self.live_logs_context = catching_logs(
  448. log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
  449. )
  450. else:
  451. self.live_logs_context = dummy_context_manager()
  452. class _LiveLoggingStreamHandler(logging.StreamHandler):
  453. """
  454. Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
  455. in each test.
  456. During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
  457. and won't appear in the terminal.
  458. """
  459. def __init__(self, terminal_reporter, capture_manager):
  460. """
  461. :param _pytest.terminal.TerminalReporter terminal_reporter:
  462. :param _pytest.capture.CaptureManager capture_manager:
  463. """
  464. logging.StreamHandler.__init__(self, stream=terminal_reporter)
  465. self.capture_manager = capture_manager
  466. self.reset()
  467. self.set_when(None)
  468. self._test_outcome_written = False
  469. def reset(self):
  470. """Reset the handler; should be called before the start of each test"""
  471. self._first_record_emitted = False
  472. def set_when(self, when):
  473. """Prepares for the given test phase (setup/call/teardown)"""
  474. self._when = when
  475. self._section_name_shown = False
  476. if when == "start":
  477. self._test_outcome_written = False
  478. def emit(self, record):
  479. ctx_manager = (
  480. self.capture_manager.global_and_fixture_disabled()
  481. if self.capture_manager
  482. else dummy_context_manager()
  483. )
  484. with ctx_manager:
  485. if not self._first_record_emitted:
  486. self.stream.write("\n")
  487. self._first_record_emitted = True
  488. elif self._when in ("teardown", "finish"):
  489. if not self._test_outcome_written:
  490. self._test_outcome_written = True
  491. self.stream.write("\n")
  492. if not self._section_name_shown and self._when:
  493. self.stream.section("live log " + self._when, sep="-", bold=True)
  494. self._section_name_shown = True
  495. logging.StreamHandler.emit(self, record)