Skip to content

Commit

Permalink
Merge pull request #227 from reportportal/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
HardNorth committed Nov 3, 2023
2 parents 883d2f1 + cd64d72 commit 6b04ee9
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 15 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog

## [Unreleased]
### Added
- Issue [#225](https://github.com/reportportal/client-Python/issues/225): JSON decoding error logging, by @HardNorth
### Fixed
- Issue [#226](https://github.com/reportportal/client-Python/issues/226): Logging batch flush on client close, by @HardNorth

## [5.5.3]
### Fixed
- Python 3.7 support, by @HardNorth
- Launch UUID attribute for AIO clients, by @HardNorth
Expand Down
1 change: 0 additions & 1 deletion reportportal_client/_internal/aio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

_T = TypeVar('_T')


DEFAULT_TASK_TRIGGER_NUM: int = 10
DEFAULT_TASK_TRIGGER_INTERVAL: float = 1.0

Expand Down
1 change: 0 additions & 1 deletion reportportal_client/_internal/services/client_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from .constants import CLIENT_ID_PROPERTY, RP_FOLDER_PATH, \
RP_PROPERTIES_FILE_PATH


logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

Expand Down
12 changes: 6 additions & 6 deletions reportportal_client/_internal/services/client_id.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

from typing import Optional, Text

def _read_client_id() -> Optional[Text]:
pass

def _store_client_id(client_id: Text) -> None:
pass
def _read_client_id() -> Optional[Text]: ...

def get_client_id() -> Text:
pass

def _store_client_id(client_id: Text) -> None: ...


def get_client_id() -> Text: ...
2 changes: 1 addition & 1 deletion reportportal_client/_internal/services/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
import certifi
import requests

from reportportal_client.helpers import get_package_parameters
from reportportal_client._internal.services.client_id import get_client_id
from reportportal_client._internal.services.constants import CLIENT_INFO, ENDPOINT
from reportportal_client.helpers import get_package_parameters

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion reportportal_client/_internal/static/defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"""This module provides RP client static objects and variables."""

import aenum as enum
from reportportal_client.helpers import ATTRIBUTE_LENGTH_LIMIT as ATTRIBUTE_LIMIT

from reportportal_client.helpers import ATTRIBUTE_LENGTH_LIMIT as ATTRIBUTE_LIMIT

RP_LOG_LEVELS = {
60000: 'UNKNOWN',
Expand Down
2 changes: 2 additions & 0 deletions reportportal_client/aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ def clone(self) -> 'AsyncRPClient':

async def close(self) -> None:
"""Close current client connections."""
await self.__client.log_batch(self._log_batcher.flush())
await self.__client.close()


Expand Down Expand Up @@ -1307,6 +1308,7 @@ def log(self, time: str, message: str, level: Optional[Union[int, str]] = None,

def close(self) -> None:
"""Close current client connections."""
self.finish_tasks()
if self.own_client:
self.create_task(self.__client.close()).blocking_result()

Expand Down
1 change: 1 addition & 0 deletions reportportal_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ def clone(self) -> 'RPClient':

def close(self) -> None:
"""Close current client connections."""
self._log(self._log_batcher.flush())
self.session.close()

def __getstate__(self) -> Dict[str, Any]:
Expand Down
14 changes: 11 additions & 3 deletions reportportal_client/core/rp_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import logging
from json import JSONDecodeError
from typing import Any, Optional, Generator, Mapping, Tuple
from typing import Any, Optional, Generator, Mapping, Tuple, Union

from aiohttp import ClientResponse
from requests import Response
Expand All @@ -41,6 +41,12 @@ def _iter_json_messages(json: Any) -> Generator[str, None, None]:
yield message


def _get_json_decode_error_message(response: Union[Response, ClientResponse]) -> str:
status = getattr(response, 'status', getattr(response, 'status_code'))
return f'Unable to decode JSON response, got {"passed" if response.ok else "failed"} ' \
f'response with code "{status}" please check your endpoint configuration or API key'


class RPResponse:
"""Class representing ReportPortal API response."""

Expand Down Expand Up @@ -82,7 +88,8 @@ def json(self) -> Any:
if self.__json is NOT_SET:
try:
self.__json = self._resp.json()
except (JSONDecodeError, TypeError):
except (JSONDecodeError, TypeError) as exc:
logger.error(_get_json_decode_error_message(self._resp), exc_info=exc)
self.__json = None
return self.__json

Expand Down Expand Up @@ -149,7 +156,8 @@ async def json(self) -> Any:
if self.__json is NOT_SET:
try:
self.__json = await self._resp.json()
except (JSONDecodeError, TypeError):
except (JSONDecodeError, TypeError) as exc:
logger.error(_get_json_decode_error_message(self._resp), exc_info=exc)
self.__json = None
return self.__json

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from setuptools import setup, find_packages

__version__ = '5.5.3'
__version__ = '5.5.4'

TYPE_STUBS = ['*.pyi']

Expand Down
18 changes: 18 additions & 0 deletions tests/aio/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pytest

from reportportal_client.aio import AsyncRPClient
from reportportal_client.core.rp_requests import AsyncRPRequestLog
from reportportal_client.helpers import timestamp


Expand Down Expand Up @@ -171,3 +172,20 @@ async def test_start_item_tracking(async_client: AsyncRPClient):

await async_client.finish_test_item(actual_item_id, timestamp())
assert async_client.current_item() is None


@pytest.mark.skipif(sys.version_info < (3, 8),
reason='the test requires AsyncMock which was introduced in Python 3.8')
@pytest.mark.asyncio
async def test_logs_flush_on_close(async_client: AsyncRPClient):
# noinspection PyTypeChecker
client: mock.Mock = async_client.client
batcher: mock.Mock = mock.Mock()
batcher.flush.return_value = [AsyncRPRequestLog('test_launch_uuid', timestamp(), message='test_message')]
async_client._log_batcher = batcher

await async_client.close()

batcher.flush.assert_called_once()
client.log_batch.assert_called_once()
client.close.assert_called_once()
19 changes: 19 additions & 0 deletions tests/aio/test_batched_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

import pickle
import sys
from unittest import mock
Expand All @@ -18,6 +19,7 @@
import pytest

from reportportal_client.aio import BatchedRPClient
from reportportal_client.core.rp_requests import AsyncRPRequestLog
from reportportal_client.helpers import timestamp


Expand Down Expand Up @@ -137,3 +139,20 @@ def test_launch_uuid_usage(launch_uuid, method, params):
assert args[0].blocking_result() == actual_launch_uuid
for i, param in enumerate(params):
assert args[i + 1] == param


@pytest.mark.skipif(sys.version_info < (3, 8),
reason='the test requires AsyncMock which was introduced in Python 3.8')
def test_logs_flush_on_close(batched_client: BatchedRPClient):
batched_client.own_client = True
# noinspection PyTypeChecker
client: mock.Mock = batched_client.client
batcher: mock.Mock = mock.Mock()
batcher.flush.return_value = [AsyncRPRequestLog('test_launch_uuid', timestamp(), message='test_message')]
batched_client._log_batcher = batcher

batched_client.close()

batcher.flush.assert_called_once()
client.log_batch.assert_called_once()
client.close.assert_called_once()
19 changes: 19 additions & 0 deletions tests/aio/test_threaded_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

import pickle
import sys
from unittest import mock

import pytest

from reportportal_client.aio import ThreadedRPClient
from reportportal_client.core.rp_requests import AsyncRPRequestLog
from reportportal_client.helpers import timestamp


Expand Down Expand Up @@ -133,3 +135,20 @@ def test_launch_uuid_usage(launch_uuid, method, params):
assert args[0].blocking_result() == actual_launch_uuid
for i, param in enumerate(params):
assert args[i + 1] == param


@pytest.mark.skipif(sys.version_info < (3, 8),
reason='the test requires AsyncMock which was introduced in Python 3.8')
def test_logs_flush_on_close(batched_client: ThreadedRPClient):
batched_client.own_client = True
# noinspection PyTypeChecker
client: mock.Mock = batched_client.client
batcher: mock.Mock = mock.Mock()
batcher.flush.return_value = [AsyncRPRequestLog('test_launch_uuid', timestamp(), message='test_message')]
batched_client._log_batcher = batcher

batched_client.close()

batcher.flush.assert_called_once()
client.log_batch.assert_called_once()
client.close.assert_called_once()
18 changes: 17 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
# noinspection PyPackageRequirements
from pytest import fixture

from reportportal_client.aio.client import Client, AsyncRPClient, BatchedRPClient, ThreadedRPClient
from reportportal_client.client import RPClient
from reportportal_client.aio.client import Client, AsyncRPClient


@fixture
Expand Down Expand Up @@ -65,3 +65,19 @@ def async_client():
client = AsyncRPClient('http://endpoint', 'project', api_key='api_key',
client=mock.AsyncMock())
return client


@fixture
def batched_client():
"""Prepare instance of the AsyncRPClient for testing."""
client = BatchedRPClient('http://endpoint', 'project', api_key='api_key',
client=mock.AsyncMock())
return client


@fixture
def threaded_client():
"""Prepare instance of the AsyncRPClient for testing."""
client = ThreadedRPClient('http://endpoint', 'project', api_key='api_key',
client=mock.AsyncMock())
return client
56 changes: 56 additions & 0 deletions tests/core/test_rp_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) 2023 EPAM Systems
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

import json
import sys
import pytest

from unittest import mock

from reportportal_client.core.rp_responses import RPResponse, AsyncRPResponse


def json_error():
raise json.JSONDecodeError('Expecting value: line 1 column 1 (char 0)', '<html />', 0)


@mock.patch('reportportal_client.core.rp_responses.logging.Logger.error')
def test_json_decode_error(error_log):
response = mock.Mock()
response.ok = False
del response.status
response.status_code = 404
response.json.side_effect = json_error

rp_response = RPResponse(response)
assert rp_response.json is None
error_log.assert_called_once()
assert error_log.call_args_list[0][0][0] == ('Unable to decode JSON response, got failed response with code "404" '
'please check your endpoint configuration or API key')


@pytest.mark.skipif(sys.version_info < (3, 8),
reason='the test requires AsyncMock which was introduced in Python 3.8')
@mock.patch('reportportal_client.core.rp_responses.logging.Logger.error')
@pytest.mark.asyncio
async def test_json_decode_error_async(error_log):
response = mock.AsyncMock()
response.ok = False
response.status = 403
response.json.side_effect = json_error

rp_response = AsyncRPResponse(response)
assert await rp_response.json is None
error_log.assert_called_once()
assert error_log.call_args_list[0][0][0] == ('Unable to decode JSON response, got failed response with code "403" '
'please check your endpoint configuration or API key')
15 changes: 15 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from requests.exceptions import ReadTimeout

from reportportal_client import RPClient
from reportportal_client.core.rp_requests import RPRequestLog
from reportportal_client.helpers import timestamp


Expand Down Expand Up @@ -293,3 +294,17 @@ def test_http_timeout_bypass(method, call_method, arguments):
kwargs = getattr(session, call_method).call_args_list[0][1]
assert 'timeout' in kwargs
assert kwargs['timeout'] == http_timeout


def test_logs_flush_on_close(rp_client: RPClient):
# noinspection PyTypeChecker
session: mock.Mock = rp_client.session
batcher: mock.Mock = mock.Mock()
batcher.flush.return_value = [RPRequestLog('test_launch_uuid', timestamp(), message='test_message')]
rp_client._log_batcher = batcher

rp_client.close()

batcher.flush.assert_called_once()
session.post.assert_called_once()
session.close.assert_called_once()

0 comments on commit 6b04ee9

Please sign in to comment.