Skip to content

Commit 6820401

Browse files
committed
added new flag to support thread logs
1 parent 5dba98e commit 6820401

File tree

4 files changed

+116
-2
lines changed

4 files changed

+116
-2
lines changed

pytest_reportportal/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def __init__(self, pytest_config):
6060
self.rp_log_level = get_actual_log_level(pytest_config, 'rp_log_level')
6161
self.rp_log_format = self.find_option(pytest_config, 'rp_log_format')
6262
self.rp_mode = self.find_option(pytest_config, 'rp_mode')
63+
self.rp_thread_logging = self.find_option(
64+
pytest_config, 'rp_thread_logging'
65+
)
6366
self.rp_parent_item_id = self.find_option(pytest_config,
6467
'rp_parent_item_id')
6568
self.rp_project = self.find_option(pytest_config,

pytest_reportportal/plugin.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from pytest_reportportal import LAUNCH_WAIT_TIMEOUT
1818
from .config import AgentConfig
19-
from .rp_logging import patching_logger_class
19+
from .rp_logging import patching_logger_class, patching_thread_class
2020
from .service import PyTestServiceClass
2121

2222
log = logging.getLogger(__name__)
@@ -204,6 +204,24 @@ def pytest_configure(config):
204204
config.workerinput['py_test_service'])
205205

206206

207+
@pytest.hookimpl(hookwrapper=True)
208+
def pytest_runtestloop(session):
209+
"""
210+
Control start and finish of all test items in the session.
211+
212+
:param session: pytest.Session
213+
:return: generator object
214+
"""
215+
config = session.config
216+
if not config._rp_enabled:
217+
yield
218+
return
219+
220+
agent_config = config._reporter_config
221+
with patching_thread_class(agent_config):
222+
yield
223+
224+
207225
@pytest.hookimpl(hookwrapper=True)
208226
def pytest_runtest_protocol(item):
209227
"""
@@ -341,6 +359,14 @@ def add_shared_option(name, help, default=None, action='store'):
341359
help='Visibility of current launch [DEFAULT, DEBUG]',
342360
default='DEFAULT'
343361
)
362+
add_shared_option(
363+
name='rp_thread_logging',
364+
help='EXPERIMENTAL: Report logs from threads. '
365+
'This option applies a patch to the builtin Thread class, '
366+
'and so it is turned off by default. Use with caution.',
367+
default=False,
368+
action='store_true'
369+
)
344370

345371
parser.addini(
346372
'rp_launch_attributes',

pytest_reportportal/rp_logging.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,94 @@
11
"""RPLogger class for low-level logging in tests."""
22

33
import logging
4+
import threading
45
from contextlib import contextmanager
56
from functools import wraps
7+
8+
from reportportal_client.client import RPClient
9+
10+
from reportportal_client._local import current, set_current
611
from reportportal_client import RPLogger
712

813

14+
@contextmanager
15+
def patching_thread_class(config):
16+
"""
17+
Add patch for Thread class.
18+
19+
Set the parent thread client as the child thread's local client
20+
"""
21+
if not config.rp_thread_logging:
22+
# Do nothing
23+
yield
24+
else:
25+
original_start = threading.Thread.start
26+
original_run = threading.Thread.run
27+
try:
28+
def wrap_start(original_func):
29+
@wraps(original_func)
30+
def _start(self, *args, **kwargs):
31+
"""Save the invoking thread's client if there is one."""
32+
# Prevent an endless loop of workers being spawned
33+
if "_monitor" not in self.name:
34+
current_client = current()
35+
self.parent_rp_client = current_client
36+
return original_func(self, *args, **kwargs)
37+
38+
return _start
39+
40+
def wrap_run(original_func):
41+
@wraps(original_func)
42+
def _run(self, *args, **kwargs):
43+
"""Create a new client for the invoked thread."""
44+
client = None
45+
if (
46+
hasattr(self, "parent_rp_client")
47+
and self.parent_rp_client
48+
and not current()
49+
):
50+
parent = self.parent_rp_client
51+
client = RPClient(
52+
endpoint=parent.endpoint,
53+
project=parent.project,
54+
token=parent.token,
55+
log_batch_size=parent.log_batch_size,
56+
is_skipped_an_issue=parent.is_skipped_an_issue,
57+
verify_ssl=parent.verify_ssl,
58+
retries=config.rp_retries,
59+
launch_id=parent.launch_id
60+
)
61+
if parent.current_item():
62+
client._item_stack.append(
63+
parent.current_item()
64+
)
65+
client.start()
66+
try:
67+
return original_func(self, *args, **kwargs)
68+
finally:
69+
if client:
70+
# Stop the client and remove any references
71+
client.terminate()
72+
self.parent_rp_client = None
73+
del self.parent_rp_client
74+
set_current(None)
75+
76+
return _run
77+
78+
if not hasattr(threading.Thread, "patched"):
79+
# patch
80+
threading.Thread.patched = True
81+
threading.Thread.start = wrap_start(original_start)
82+
threading.Thread.run = wrap_run(original_run)
83+
yield
84+
85+
finally:
86+
if hasattr(threading.Thread, "patched"):
87+
threading.Thread.start = original_start
88+
threading.Thread.run = original_run
89+
del threading.Thread.patched
90+
91+
992
@contextmanager
1093
def patching_logger_class():
1194
"""

tests/unit/test_plugin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ def test_pytest_addoption_adds_correct_ini_file_arguments():
318318
'rp_uuid',
319319
'rp_endpoint',
320320
'rp_mode',
321+
'rp_thread_logging',
321322
'rp_launch_attributes',
322323
'rp_tests_attributes',
323324
'rp_log_batch_size',
@@ -361,7 +362,8 @@ def test_pytest_addoption_adds_correct_command_line_arguments():
361362
'--rp-parent-item-id',
362363
'--rp-uuid',
363364
'--rp-endpoint',
364-
'--rp-mode'
365+
'--rp-mode',
366+
'--rp-thread-logging'
365367
)
366368
mock_parser = mock.MagicMock(spec=Parser)
367369
mock_reporting_group = mock_parser.getgroup.return_value

0 commit comments

Comments
 (0)