Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release #170

Merged
merged 14 commits into from
Jul 11, 2023
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 119
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ on:

jobs:
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '2.7', '3.6', '3.7', '3.8', '3.9', '3.10' ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand All @@ -53,7 +53,7 @@ jobs:
run: tox

- name: Upload coverage to Codecov
if: matrix.python-version == 3.6 && success()
if: matrix.python-version == 3.7 && success()
uses: codecov/codecov-action@v3
with:
files: coverage.xml
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Changelog

## [Unreleased]
### Added
- `RP_LAUNCH_UUID_PRINT` and `RP_LAUNCH_UUID_PRINT_OUTPUT` configuration variables, by @HardNorth
### Changed
- Internal item list was replaced with LifoQueue, by @HardNorth
- Client version updated on [5.4.0](https://github.com/reportportal/client-Python/releases/tag/5.4.0), by @HardNorth
- `service.RobotService.init_service` method now accepts `variables.Variables` object as single argument instead of many of them, which were just copy-paste, by @HardNorth
### Removed
- Python 2.7, 3.6 support, by @HardNorth

## [5.3.3]
### Changed
- Client version updated on [5.3.5](https://github.com/reportportal/client-Python/releases/tag/5.3.5), by @HardNorth
- The Agent publish a warning and does not report anything in case of a missed required variable now, by @HardNorth
Expand Down
5 changes: 5 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Dev dependencies
pytest
pytest-cov
delayed_assert
robotframework-datadriver
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Basic dependencies
python-dateutil>=2.8.1
reportportal-client==5.3.5
reportportal-client==5.4.0
robotframework
six>=1.15.0
17 changes: 0 additions & 17 deletions robotframework_reportportal/exception.py

This file was deleted.

104 changes: 55 additions & 49 deletions robotframework_reportportal/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
import os
from functools import wraps
from mimetypes import guess_type
from typing import Optional, Dict, Union, Any
from queue import LifoQueue
from warnings import warn

from reportportal_client.helpers import gen_attributes

from .exception import RobotServiceException
from .model import Keyword, Launch, Test, LogMessage, Suite
from .service import RobotService
from .static import MAIN_SUITE_ID, PABOT_WIHOUT_LAUNCH_ID_MSG
Expand All @@ -30,6 +32,13 @@
logger = logging.getLogger(__name__)


class _LifoQueue(LifoQueue):
def last(self):
with self.mutex:
if self._qsize():
return self.queue[-1]


def check_rp_enabled(func):
"""Verify is RP is enabled in config."""
@wraps(func)
Expand All @@ -41,18 +50,22 @@ def wrap(*args, **kwargs):
return wrap


class listener(object):
# noinspection PyPep8Naming
class listener:
"""Robot Framework listener interface for reporting to Report Portal."""

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

def __init__(self):
def __init__(self) -> None:
"""Initialize listener attributes."""
self._items = []
self._items = _LifoQueue()
self._service = None
self._variables = None

def _build_msg_struct(self, message):
def _build_msg_struct(self, message: Dict) -> LogMessage:
"""Check if the given message comes from our custom logger or not.

:param message: Message passed by the Robot Framework
Expand All @@ -66,18 +79,21 @@ def _build_msg_struct(self, message):
msg.item_id = getattr(self.current_item, 'rp_item_id', None)
return msg

def _finish_current_item(self):
"""Remove the last item from the self._items list."""
return self._items.pop()
def _add_current_item(self, item: Union[Keyword, Launch, Suite, Test]) -> None:
"""Add the last item from the self._items queue."""
self._items.put(item)

def _remove_current_item(self) -> Union[Keyword, Launch, Suite, Test]:
"""Remove the last item from the self._items queue."""
return self._items.get()

@property
def current_item(self):
"""Get the last item from the self._items list."""
if self._items:
return self._items[-1]
def current_item(self) -> Optional[Union[Keyword, Launch, Suite, Test]]:
"""Get the last item from the self._items queue."""
return self._items.last()

@check_rp_enabled
def log_message(self, message):
def log_message(self, message: Dict) -> None:
"""Send log message to the Report Portal.

:param message: Message passed by the Robot Framework
Expand All @@ -87,7 +103,7 @@ def log_message(self, message):
self.service.log(message=msg)

@check_rp_enabled
def log_message_with_image(self, msg, image):
def log_message_with_image(self, msg: Dict, image: str):
"""Send log message to the Report Portal.

:param msg: Message passed by the Robot Framework
Expand All @@ -105,37 +121,27 @@ def log_message_with_image(self, msg, image):
self.service.log(message=mes)

@property
def parent_id(self):
def parent_id(self) -> Optional[str]:
"""Get rp_item_id attribute of the current item."""
return getattr(self.current_item, 'rp_item_id', None)

@property
def service(self):
def service(self) -> RobotService:
"""Initialize instance of the RobotService."""
if self.variables.enabled and self._service is None:
if self.variables.enabled and not self._service:
self._service = RobotService()
self._service.init_service(
endpoint=self.variables.endpoint,
project=self.variables.project,
api_key=self.variables.api_key,
log_batch_size=self.variables.log_batch_size,
pool_size=self.variables.pool_size,
skipped_issue=self.variables.skipped_issue,
verify_ssl=self.variables.verify_ssl,
log_batch_payload_size=self.variables.log_batch_payload_size,
launch_id=self.variables.launch_id,
)
self._service.init_service(self.variables)
return self._service

@property
def variables(self):
def variables(self) -> Variables:
"""Get instance of the variables.Variables class."""
if not self._variables:
self._variables = Variables()
return self._variables

@check_rp_enabled
def start_launch(self, attributes, ts=None):
def start_launch(self, attributes: Dict, ts: Optional[Any] = None) -> None:
"""Start a new launch at the Report Portal.

:param attributes: Dictionary passed by the Robot Framework
Expand All @@ -146,7 +152,7 @@ def start_launch(self, attributes, ts=None):
launch.doc = self.variables.launch_doc or launch.doc
if not self.variables.launch_id:
if self.variables.pabot_used:
raise RobotServiceException(PABOT_WIHOUT_LAUNCH_ID_MSG)
warn(PABOT_WIHOUT_LAUNCH_ID_MSG, stacklevel=2)
logger.debug('ReportPortal - Start Launch: {0}'.format(
launch.attributes))
self.service.start_launch(
Expand All @@ -159,7 +165,7 @@ def start_launch(self, attributes, ts=None):
self.service.rp.launch_id = self.variables.launch_id

@check_rp_enabled
def start_suite(self, name, attributes, ts=None):
def start_suite(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> None:
"""Start a new test suite at the Report Portal.

:param name: Test suite name
Expand All @@ -175,23 +181,23 @@ def start_suite(self, name, attributes, ts=None):
.format(attributes))
suite = Suite(name, attributes)
suite.rp_item_id = self.service.start_suite(suite=suite, ts=ts)
self._items.append(suite)
self._add_current_item(suite)
else:
logger.debug('ReportPortal - Start Suite: {0}'.format(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)
self._items.append(suite)
self._add_current_item(suite)

@check_rp_enabled
def end_suite(self, _, attributes, ts=None):
def end_suite(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None) -> None:
"""Finish started test suite at the Report Portal.

:param attributes: Dictionary passed by the Robot Framework
:param ts: Timestamp(used by the ResultVisitor)
"""
if attributes['id'] == MAIN_SUITE_ID:
suite = self._finish_current_item().update(attributes)
suite = self._remove_current_item().update(attributes)
logger.debug('ReportPortal - End Suite: {0}'
.format(suite.attributes))
self.service.finish_suite(suite=suite, ts=ts)
Expand All @@ -200,13 +206,13 @@ def end_suite(self, _, attributes, ts=None):
msg='ReportPortal - End Launch: {0}'.format(attributes))
self.service.finish_launch(launch=launch, ts=ts)
else:
suite = self._finish_current_item().update(attributes)
suite = self._remove_current_item().update(attributes)
logger.debug(
'ReportPortal - End Suite: {0}'.format(suite.attributes))
self.service.finish_suite(suite=suite, ts=ts)

@check_rp_enabled
def start_test(self, name, attributes, ts=None):
def start_test(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> None:
"""Start a new test case at the Report Portal.

:param name: Test case name
Expand All @@ -223,10 +229,10 @@ def start_test(self, name, attributes, ts=None):
self.variables.test_attributes + test.tags)
test.rp_parent_item_id = self.parent_id
test.rp_item_id = self.service.start_test(test=test, ts=ts)
self._items.append(test)
self._add_current_item(test)

@check_rp_enabled
def end_test(self, _, attributes, ts=None):
def end_test(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None) -> None:
"""Finish started test case at the Report Portal.

:param attributes: Dictionary passed by the Robot Framework
Expand All @@ -240,11 +246,11 @@ def end_test(self, _, attributes, ts=None):
if test.message:
self.log_message({'message': test.message, 'level': 'DEBUG'})
logger.debug('ReportPortal - End Test: {0}'.format(test.attributes))
self._finish_current_item()
self._remove_current_item()
self.service.finish_test(test=test, ts=ts)

@check_rp_enabled
def start_keyword(self, name, attributes, ts=None):
def start_keyword(self, name: str, attributes: Dict, ts: Optional[Any] = None) -> None:
"""Start a new keyword(test step) at the Report Portal.

:param name: Keyword name
Expand All @@ -256,20 +262,20 @@ def start_keyword(self, name, attributes, ts=None):
kwd.rp_parent_item_id = self.parent_id
logger.debug('ReportPortal - Start Keyword: {0}'.format(attributes))
kwd.rp_item_id = self.service.start_keyword(keyword=kwd, ts=ts)
self._items.append(kwd)
self._add_current_item(kwd)

@check_rp_enabled
def end_keyword(self, _, attributes, ts=None):
def end_keyword(self, _: Optional[str], attributes: Dict, ts: Optional[Any] = None) -> None:
"""Finish started keyword at the Report Portal.

:param attributes: Dictionary passed by the Robot Framework
:param ts: Timestamp(used by the ResultVisitor)
"""
kwd = self._finish_current_item().update(attributes)
kwd = self._remove_current_item().update(attributes)
logger.debug('ReportPortal - End Keyword: {0}'.format(kwd.attributes))
self.service.finish_keyword(keyword=kwd, ts=ts)

def log_file(self, log_path):
def log_file(self, log_path: str) -> None:
"""Attach HTML log file created by Robot Framework to RP launch.

:param log_path: Path to the log file
Expand All @@ -278,7 +284,7 @@ def log_file(self, log_path):
message = {'message': 'Execution log', 'level': 'INFO'}
self.log_message_with_image(message, log_path)

def report_file(self, report_path):
def report_file(self, report_path: str) -> None:
"""Attach HTML report created by Robot Framework to RP launch.

:param report_path: Path to the report file
Expand All @@ -287,7 +293,7 @@ def report_file(self, report_path):
message = {'message': 'Execution report', 'level': 'INFO'}
self.log_message_with_image(message, report_path)

def xunit_file(self, xunit_path):
def xunit_file(self, xunit_path: str) -> None:
"""Attach XUnit file created by Robot Framework to RP launch.

:param xunit_path: Path to the XUnit file
Expand All @@ -297,6 +303,6 @@ def xunit_file(self, xunit_path):
self.log_message_with_image(message, xunit_path)

@check_rp_enabled
def close(self):
def close(self) -> None:
"""Call service terminate when the whole test execution is done."""
self.service.terminate_service()
Loading