Skip to content

Commit

Permalink
Merge pull request #186 from reportportal/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
HardNorth authored Mar 20, 2024
2 parents b7701e4 + 0b43da9 commit 1db0ca3
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 321 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## [Unreleased]
### Added
- Issue [#178](https://github.com/reportportal/agent-Python-RobotFramework/issues/178) Metadata attributes handling, by @HardNorth
### Changed
- Client version updated on [5.5.6](https://github.com/reportportal/client-Python/releases/tag/5.5.6), by @HardNorth
### Removed
- `model.pyi`, `static.pyi` stub files, as we don't really need them anymore, by @HardNorth

## [5.5.2]
### Added
- Binary data escaping in `listener` module (enhancing `Get Binary File` keyword logging), by @HardNorth
### Changed
- Client version updated on [5.5.5](https://github.com/reportportal/client-Python/releases/tag/5.5.5), by @HardNorth
Expand Down
6 changes: 6 additions & 0 deletions examples/suite_metadata.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*** Settings ***
Metadata Author John Doe

*** Test Cases ***
Simple test
Log Hello, world!
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Basic dependencies
python-dateutil~=2.8.1
reportportal-client~=5.5.5
reportportal-client~=5.5.6
robotframework
71 changes: 23 additions & 48 deletions robotframework_reportportal/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,19 @@
import re
from functools import wraps
from mimetypes import guess_type
from types import MappingProxyType
from typing import Optional, Dict, Union, Any
from warnings import warn

from reportportal_client.helpers import gen_attributes, LifoQueue, is_binary, guess_content_type_from_bytes
from reportportal_client.helpers import LifoQueue, is_binary, guess_content_type_from_bytes

from .model import Keyword, Launch, Test, LogMessage, Suite
from .service import RobotService
from .static import MAIN_SUITE_ID, PABOT_WIHOUT_LAUNCH_ID_MSG
from .static import MAIN_SUITE_ID, PABOT_WITHOUT_LAUNCH_ID_MSG
from .variables import Variables

logger = logging.getLogger(__name__)
VARIABLE_PATTERN = r'^\s*\${[^}]*}\s*=\s*'
TRUNCATION_SIGN = "...'"
CONTENT_TYPE_TO_EXTENSIONS = MappingProxyType({
'application/pdf': 'pdf',
'application/zip': 'zip',
'application/java-archive': 'jar',
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/bmp': 'bmp',
'image/vnd.microsoft.icon': 'ico',
'image/webp': 'webp',
'audio/mpeg': 'mp3',
'audio/wav': 'wav',
'video/mpeg': 'mpeg',
'video/avi': 'avi',
'video/webm': 'webm',
'text/plain': 'txt',
'application/octet-stream': 'bin'
})


def _unescape(binary_string: str, stop_at: int = -1):
Expand Down Expand Up @@ -106,9 +87,9 @@ def wrap(*args, **kwargs):
class listener:
"""Robot Framework listener interface for reporting to ReportPortal."""

_items: LifoQueue = ...
_service: Optional[RobotService] = ...
_variables: Optional[Variables] = ...
_items: LifoQueue
_service: Optional[RobotService]
_variables: Optional[Variables]
ROBOT_LISTENER_API_VERSION = 2

def __init__(self) -> None:
Expand Down Expand Up @@ -165,7 +146,7 @@ def log_message(self, message: Dict) -> None:
msg.message = (f'Binary data of type "{content_type}" logging skipped, as it was processed as text and'
' hence corrupted.')
msg.level = 'WARN'
logger.debug('ReportPortal - Log Message: {0}'.format(message))
logger.debug(f'ReportPortal - Log Message: {message}')
self.service.log(message=msg)

@check_rp_enabled
Expand All @@ -182,8 +163,7 @@ def log_message_with_image(self, msg: Dict, image: str):
'data': fh.read(),
'mime': guess_type(image)[0] or 'application/octet-stream'
}
logger.debug('ReportPortal - Log Message with Image: {0} {1}'
.format(mes, image))
logger.debug(f'ReportPortal - Log Message with Image: {mes} {image}')
self.service.log(message=mes)

@property
Expand All @@ -207,18 +187,17 @@ def variables(self) -> Variables:
return self._variables

@check_rp_enabled
def start_launch(self, attributes: Dict, ts: Optional[Any] = None) -> None:
def start_launch(self, attributes: Dict[str, Any], ts: Optional[Any] = None) -> None:
"""Start a new launch at the ReportPortal.
:param attributes: Dictionary passed by the Robot Framework
:param ts: Timestamp(used by the ResultVisitor)
"""
launch = Launch(self.variables.launch_name, attributes)
launch.attributes = gen_attributes(self.variables.launch_attributes)
launch = Launch(self.variables.launch_name, attributes, self.variables.launch_attributes)
launch.doc = self.variables.launch_doc or launch.doc
if self.variables.pabot_used:
warn(PABOT_WIHOUT_LAUNCH_ID_MSG, stacklevel=2)
logger.debug('ReportPortal - Start Launch: {0}'.format(launch.attributes))
warn(PABOT_WITHOUT_LAUNCH_ID_MSG, stacklevel=2)
logger.debug(f'ReportPortal - Start Launch: {launch.robot_attributes}')
self.service.start_launch(
launch=launch,
mode=self.variables.mode,
Expand All @@ -237,10 +216,10 @@ def start_suite(self, name: str, attributes: Dict, ts: Optional[Any] = None) ->
if attributes['id'] == MAIN_SUITE_ID:
self.start_launch(attributes, ts)
if self.variables.pabot_used:
name += '.{0}'.format(self.variables.pabot_pool_id)
logger.debug('ReportPortal - Create global Suite: {0}'.format(attributes))
name = f'{name}.{self.variables.pabot_pool_id}'
logger.debug(f'ReportPortal - Create global Suite: {attributes}')
else:
logger.debug('ReportPortal - Start Suite: {0}'.format(attributes))
logger.debug(f'ReportPortal - Start Suite: {attributes}')
suite = Suite(name, attributes)
suite.rp_parent_item_id = self.parent_id
suite.rp_item_id = self.service.start_suite(suite=suite, ts=ts)
Expand All @@ -255,12 +234,11 @@ def end_suite(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None
:param ts: Timestamp(used by the ResultVisitor)
"""
suite = self._remove_current_item().update(attributes)
logger.debug('ReportPortal - End Suite: {0}'.format(suite.attributes))
logger.debug(f'ReportPortal - End Suite: {suite.robot_attributes}')
self.service.finish_suite(suite=suite, ts=ts)
if attributes['id'] == MAIN_SUITE_ID:
launch = Launch(self.variables.launch_name, attributes)
logger.debug(
msg='ReportPortal - End Launch: {0}'.format(attributes))
launch = Launch(self.variables.launch_name, attributes, None)
logger.debug(msg=f'ReportPortal - End Launch: {attributes}')
self.service.finish_launch(launch=launch, ts=ts)

@check_rp_enabled
Expand All @@ -275,9 +253,8 @@ def start_test(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> N
# no 'source' parameter at this level for Robot versions < 4
attributes = attributes.copy()
attributes['source'] = getattr(self.current_item, 'source', None)
test = Test(name=name, attributes=attributes)
logger.debug('ReportPortal - Start Test: {0}'.format(attributes))
test.attributes = gen_attributes(self.variables.test_attributes + test.tags)
test = Test(name=name, robot_attributes=attributes, test_attributes=self.variables.test_attributes)
logger.debug(f'ReportPortal - Start Test: {attributes}')
test.rp_parent_item_id = self.parent_id
test.rp_item_id = self.service.start_test(test=test, ts=ts)
self._add_current_item(test)
Expand All @@ -291,13 +268,11 @@ def end_test(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None)
:param ts: Timestamp(used by the ResultVisitor)
"""
test = self.current_item.update(attributes)
test.attributes = gen_attributes(
self.variables.test_attributes + test.tags)
if not test.critical and test.status == 'FAIL':
test.status = 'SKIP'
if test.message:
self.log_message({'message': test.message, 'level': 'DEBUG'})
logger.debug('ReportPortal - End Test: {0}'.format(test.attributes))
logger.debug(f'ReportPortal - End Test: {test.robot_attributes}')
self._remove_current_item()
self.service.finish_test(test=test, ts=ts)

Expand All @@ -309,9 +284,9 @@ def start_keyword(self, name: str, attributes: Dict, ts: Optional[Any] = None) -
:param attributes: Dictionary passed by the Robot Framework
:param ts: Timestamp(used by the ResultVisitor)
"""
kwd = Keyword(name=name, parent_type=self.current_item.type, attributes=attributes)
kwd = Keyword(name=name, parent_type=self.current_item.type, robot_attributes=attributes)
kwd.rp_parent_item_id = self.parent_id
logger.debug('ReportPortal - Start Keyword: {0}'.format(attributes))
logger.debug(f'ReportPortal - Start Keyword: {attributes}')
kwd.rp_item_id = self.service.start_keyword(keyword=kwd, ts=ts)
self._add_current_item(kwd)

Expand All @@ -324,7 +299,7 @@ def end_keyword(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = No
:param ts: Timestamp(used by the ResultVisitor)
"""
kwd = self._remove_current_item().update(attributes)
logger.debug('ReportPortal - End Keyword: {0}'.format(kwd.attributes))
logger.debug(f'ReportPortal - End Keyword: {kwd.robot_attributes}')
self.service.finish_keyword(keyword=kwd, ts=ts)

def log_file(self, log_path: str) -> None:
Expand Down
53 changes: 25 additions & 28 deletions robotframework_reportportal/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,29 @@ def log_free_memory(self):
},
)
"""
from typing import Optional, Dict

from robot.api import logger

from .model import LogMessage


def write(msg, level='INFO', html=False, attachment=None, launch_log=False):
def write(msg: str, level: str = 'INFO', html: bool = False, attachment: Optional[Dict[str, str]] = None,
launch_log: bool = False) -> None:
"""Write the message to the log file using the given level.
Valid log levels are ``TRACE``, ``DEBUG``, ``INFO`` (default since RF
2.9.1), ``WARN``, and ``ERROR`` (new in RF 2.9). Additionally it is
possible to use ``HTML`` pseudo log level that logs the message as HTML
using the ``INFO`` level.
Valid log levels are ``TRACE``, ``DEBUG``, ``INFO`` (default since RF 2.9.1), ``WARN``,
and ``ERROR`` (new in RF 2.9). Additionally, it is possible to use ``HTML`` pseudo log level that logs the message
as HTML using the ``INFO`` level.
Attachment should contain a dict with "name", "data" and "mime" values
defined. See module example.
Attachment should contain a dict with "name", "data" and "mime" values defined. See module example.
Instead of using this method, it is generally better to use the level
specific methods such as ``info`` and ``debug`` that have separate
Instead of using this method, it is generally better to use the level specific methods such as ``info`` and
``debug`` that have separate
:param msg: argument to control the message format.
:param level: log level
:param html: format or not format the message as html.
:param msg: argument to control the message format.
:param level: log level
:param html: format or not format the message as html.
:param attachment: a binary content to attach to the log entry
:param launch_log: put the log entry on Launch level
"""
Expand All @@ -68,46 +68,43 @@ def write(msg, level='INFO', html=False, attachment=None, launch_log=False):
logger.write(log_message, level, html)


def trace(msg, html=False, attachment=None, launch_log=False):
def trace(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None:
"""Write the message to the log file using the ``TRACE`` level."""
write(msg, "TRACE", html, attachment, launch_log)


def debug(msg, html=False, attachment=None, launch_log=False):
def debug(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None:
"""Write the message to the log file using the ``DEBUG`` level."""
write(msg, "DEBUG", html, attachment, launch_log)


def info(msg, html=False, also_console=False, attachment=None,
launch_log=False):
def info(msg: str, html: bool = False, also_console: bool = False, attachment: Optional[Dict[str, str]] = None,
launch_log: bool = False):
"""Write the message to the log file using the ``INFO`` level.
If ``also_console`` argument is set to ``True``, the message is
written both to the log file and to the console.
If ``also_console`` argument is set to ``True``, the message is written both to the log file and to the console.
"""
write(msg, "INFO", html, attachment, launch_log)
if also_console:
console(msg)


def warn(msg, html=False, attachment=None, launch_log=False):
def warn(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None:
"""Write the message to the log file using the ``WARN`` level."""
write(msg, "WARN", html, attachment, launch_log)
write(msg, 'WARN', html, attachment, launch_log)


def error(msg, html=False, attachment=None, launch_log=False):
def error(msg: str, html: bool = False, attachment: Optional[Dict[str, str]] = None, launch_log: bool = False) -> None:
"""Write the message to the log file using the ``ERROR`` level."""
write(msg, "ERROR", html, attachment, launch_log)
write(msg, 'ERROR', html, attachment, launch_log)


def console(msg, newline=True, stream="stdout"):
def console(msg: str, newline: bool = True, stream: str = 'stdout') -> None:
"""Write the message to the console.
If the ``newline`` argument is ``True``, a newline character is
automatically added to the message.
If the ``newline`` argument is ``True``, a newline character is automatically added to the message.
By default the message is written to the standard output stream.
Using the standard error stream is possibly by giving the ``stream``
argument value ``'stderr'``.
By default, the message is written to the standard output stream.
Using the standard error stream is possibly by giving the ``stream`` argument value ``'stderr'``.
"""
logger.console(msg, newline, stream)
Loading

0 comments on commit 1db0ca3

Please sign in to comment.