Skip to content

Commit 29a817e

Browse files
authored
Merge pull request #43 from s0undt3ch/features/auto-logging
Automated logs reporting for pytest >= 3.3
2 parents 4764266 + 682639f commit 29a817e

File tree

3 files changed

+116
-4
lines changed

3 files changed

+116
-4
lines changed

pytest_reportportal/listener.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
import cgi
22
import pytest
3+
import logging
34

45
from .service import PyTestService
56

7+
try:
8+
# This try/except can go away once we support pytest >= 3.3
9+
import _pytest.logging
10+
PYTEST_HAS_LOGGING_PLUGIN = True
11+
from .rp_logging import RPLogHandler, patching_logger_class
12+
except ImportError:
13+
PYTEST_HAS_LOGGING_PLUGIN = False
14+
615

716
class RPReportListener(object):
8-
def __init__(self):
17+
def __init__(self, log_level=logging.NOTSET):
918
# Test Item result
1019
self.result = None
20+
self._log_level = log_level
21+
if PYTEST_HAS_LOGGING_PLUGIN:
22+
self._log_handler = RPLogHandler(log_level, filter_reportportal_client_logs=True)
1123

1224
@pytest.hookimpl(hookwrapper=True)
1325
def pytest_runtest_protocol(self, item):
1426
PyTestService.start_pytest_item(item)
15-
yield
27+
if PYTEST_HAS_LOGGING_PLUGIN:
28+
# This check can go away once we support pytest >= 3.3
29+
with patching_logger_class():
30+
with _pytest.logging.catching_logs(self._log_handler,
31+
level=self._log_level):
32+
yield
33+
else:
34+
yield
1635
PyTestService.finish_pytest_item(self.result or 'SKIPPED')
1736

1837
@pytest.hookimpl(hookwrapper=True)

pytest_reportportal/plugin.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
from .service import PyTestService
77
from .listener import RPReportListener
88

9+
try:
10+
# This try/except can go away once we support pytest >= 3.3
11+
import _pytest.logging
12+
PYTEST_HAS_LOGGING_PLUGIN = True
13+
except ImportError:
14+
PYTEST_HAS_LOGGING_PLUGIN = False
15+
916

1017
def pytest_sessionstart(session):
1118
if session.config.getoption('--collect-only', default=False) is True:
@@ -45,7 +52,18 @@ def pytest_configure(config):
4552
"pytest report portal is not compatible with 'xdist' plugin.")
4653

4754
# set Pytest_Reporter and configure it
48-
config._reporter = RPReportListener()
55+
56+
if PYTEST_HAS_LOGGING_PLUGIN:
57+
# This check can go away once we support pytest >= 3.3
58+
try:
59+
config._reporter = RPReportListener(
60+
_pytest.logging.get_actual_log_level(config, 'rp_log_level')
61+
)
62+
except TypeError:
63+
# No log level set either in INI or CLI
64+
config._reporter = RPReportListener()
65+
else:
66+
config._reporter = RPReportListener()
4967

5068
if hasattr(config, '_reporter'):
5169
config.pluginmanager.register(config._reporter)
@@ -69,6 +87,19 @@ def pytest_addoption(parser):
6987
dest='rp_launch',
7088
help='Launch name (overrides rp_launch config option)')
7189

90+
if PYTEST_HAS_LOGGING_PLUGIN:
91+
group.addoption(
92+
'--rp-log-level',
93+
dest='rp_log_level',
94+
default=logging.NOTSET,
95+
help='Logging level for automated log records reporting'
96+
)
97+
parser.addini(
98+
'rp_log_level',
99+
default=logging.NOTSET,
100+
help='Logging level for automated log records reporting'
101+
)
102+
72103
parser.addini(
73104
'rp_uuid',
74105
help='UUID')

pytest_reportportal/rp_logging.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import sys
22
import logging
3+
from contextlib import contextmanager
4+
from functools import wraps
35

46
from .service import PyTestService
57

@@ -48,8 +50,19 @@ class RPLogHandler(logging.Handler):
4850
}
4951
_sorted_levelnos = sorted(_loglevel_map.keys(), reverse=True)
5052

51-
def __init__(self, level=logging.NOTSET):
53+
def __init__(self, level=logging.NOTSET,
54+
filter_reportportal_client_logs=False):
5255
super(RPLogHandler, self).__init__(level)
56+
self.filter_reportportal_client_logs = filter_reportportal_client_logs
57+
58+
def filter(self, record):
59+
if self.filter_reportportal_client_logs is False:
60+
return True
61+
if record.name.startswith('reportportal_client'):
62+
# Don't send reportportal_client logs.
63+
# Specially because we'll hit a max recursion issue
64+
return False
65+
return True
5366

5467
def emit(self, record):
5568
msg = ''
@@ -70,3 +83,52 @@ def emit(self, record):
7083
loglevel=self._loglevel_map[level],
7184
attachment=record.__dict__.get('attachment', None),
7285
)
86+
87+
88+
@contextmanager
89+
def patching_logger_class():
90+
logger_class = logging.getLoggerClass()
91+
original_log = logger_class._log
92+
original_makeRecord = logger_class.makeRecord
93+
94+
try:
95+
def wrap_log(original_func):
96+
@wraps(original_func)
97+
def _log(self, *args, **kwargs):
98+
attachment = kwargs.pop('attachment', None)
99+
if attachment is not None:
100+
kwargs.setdefault('extra', {}).update(
101+
{'attachment': attachment})
102+
return original_func(self, *args, **kwargs)
103+
return _log
104+
105+
def wrap_makeRecord(original_func):
106+
@wraps(original_func)
107+
def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
108+
func=None, extra=None, sinfo=None):
109+
if extra is not None:
110+
attachment = extra.pop('attachment', None)
111+
else:
112+
attachment = None
113+
try:
114+
# Python 3.5
115+
record = original_func(self, name, level, fn, lno, msg,
116+
args, exc_info, func=func,
117+
extra=extra, sinfo=sinfo)
118+
except TypeError:
119+
# Python 2.7
120+
record = original_func(self, name, level, fn, lno, msg,
121+
args, exc_info, func=func,
122+
extra=extra)
123+
record.attachment = attachment
124+
return record
125+
return makeRecord
126+
127+
logger_class._log = wrap_log(logger_class._log)
128+
logger_class.makeRecord = wrap_makeRecord(logger_class.makeRecord)
129+
130+
yield
131+
132+
finally:
133+
logger_class._log = original_log
134+
logger_class.makeRecord = original_makeRecord

0 commit comments

Comments
 (0)