diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc6b0c..71e542f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] ### Fixed +- Attribute truncation for every method with attributes, by @HardNorth + +## [5.5.1] +### Fixed - Multipart file upload for Async clients, by @HardNorth ## [5.5.0] diff --git a/reportportal_client/__init__.py b/reportportal_client/__init__.py index d0bb0e0..340801f 100644 --- a/reportportal_client/__init__.py +++ b/reportportal_client/__init__.py @@ -74,6 +74,8 @@ def create_client( :type launch_uuid_print: bool :param print_output: Set output stream for Launch UUID printing. :type print_output: OutputType + :param truncate_attributes: Truncate test item attributes to default maximum length. + :type truncate_attributes: bool :param log_batch_size: Option to set the maximum number of logs that can be processed in one batch. :type log_batch_size: int diff --git a/reportportal_client/aio/client.py b/reportportal_client/aio/client.py index 5818ac1..3fab8cb 100644 --- a/reportportal_client/aio/client.py +++ b/reportportal_client/aio/client.py @@ -88,6 +88,7 @@ class Client: mode: str launch_uuid_print: bool print_output: OutputType + truncate_attributes: bool _skip_analytics: str _session: Optional[RetryingClientSession] __stat_task: Optional[asyncio.Task] @@ -107,6 +108,7 @@ def __init__( mode: str = 'DEFAULT', launch_uuid_print: bool = False, print_output: OutputType = OutputType.STDOUT, + truncate_attributes: bool = True, **kwargs: Any ) -> None: """Initialize the class instance with arguments. @@ -125,6 +127,7 @@ def __init__( :param mode: Launch mode, all Launches started by the client will be in that mode. :param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout. :param print_output: Set output stream for Launch UUID printing. + :param truncate_attributes: Truncate test item attributes to default maximum length. """ self.api_v1, self.api_v2 = 'v1', 'v2' self.endpoint = endpoint @@ -144,6 +147,7 @@ def __init__( self._session = None self.__stat_task = None self.api_key = api_key + self.truncate_attributes = truncate_attributes async def session(self) -> RetryingClientSession: """Return aiohttp.ClientSession class instance, initialize it if necessary. @@ -156,7 +160,7 @@ async def session(self) -> RetryingClientSession: if self.verify_ssl is None or (type(self.verify_ssl) == bool and not self.verify_ssl): ssl_config = False else: - if type(self.verify_ssl) == str: + if type(self.verify_ssl) is str: ssl_config = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=self.verify_ssl) else: ssl_config = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=certifi.where()) @@ -242,7 +246,7 @@ async def start_launch(self, request_payload = LaunchStartRequest( name=name, start_time=start_time, - attributes=attributes, + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, description=description, mode=self.mode, rerun=rerun, @@ -306,7 +310,7 @@ async def start_test_item(self, start_time, item_type, launch_uuid, - attributes=verify_value_length(attributes), + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, code_ref=code_ref, description=description, has_stats=has_stats, @@ -355,7 +359,7 @@ async def finish_test_item(self, end_time, launch_uuid, status, - attributes=verify_value_length(attributes), + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, description=description, is_skipped_an_issue=self.is_skipped_an_issue, issue=issue, @@ -389,7 +393,7 @@ async def finish_launch(self, request_payload = LaunchFinishRequest( end_time, status=status, - attributes=verify_value_length(attributes), + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, description=kwargs.get('description') ).payload response = await AsyncHttpRequest((await self.session()).put, url=url, json=request_payload, @@ -415,7 +419,7 @@ async def update_test_item(self, """ data = { 'description': description, - 'attributes': verify_value_length(attributes), + 'attributes': verify_value_length(attributes) if self.truncate_attributes else attributes, } item_id = await self.get_item_id_by_uuid(item_uuid) url = root_uri_join(self.base_url_v1, 'item', item_id, 'update') @@ -650,6 +654,7 @@ def __init__( :param mode: Launch mode, all Launches started by the client will be in that mode. :param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout. :param print_output: Set output stream for Launch UUID printing. + :param truncate_attributes: Truncate test item attributes to default maximum length. :param client: ReportPortal async Client instance to use. If set, all above arguments will be ignored. :param launch_uuid: A launch UUID to use instead of starting own one. @@ -1009,6 +1014,7 @@ def __init__( :param mode: Launch mode, all Launches started by the client will be in that mode. :param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout. :param print_output: Set output stream for Launch UUID printing. + :param truncate_attributes: Truncate test item attributes to default maximum length. :param client: ReportPortal async Client instance to use. If set, all above arguments will be ignored. :param launch_uuid: A launch UUID to use instead of starting own one. @@ -1384,6 +1390,7 @@ def __init__( :param mode: Launch mode, all Launches started by the client will be in that mode. :param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout. :param print_output: Set output stream for Launch UUID printing. + :param truncate_attributes: Truncate test item attributes to default maximum length. :param client: ReportPortal async Client instance to use. If set, all above arguments will be ignored. :param launch_uuid: A launch UUID to use instead of starting own one. @@ -1406,7 +1413,7 @@ def __init__( self.shutdown_timeout = shutdown_timeout self.__init_task_list(task_list, task_mutex) self.__init_loop(loop) - if type(launch_uuid) == str: + if type(launch_uuid) is str: super().__init__(endpoint, project, launch_uuid=self.create_task(self.__return_value(launch_uuid)), **kwargs) else: @@ -1561,6 +1568,7 @@ def __init__( :param mode: Launch mode, all Launches started by the client will be in that mode. :param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout. :param print_output: Set output stream for Launch UUID printing. + :param truncate_attributes: Truncate test item attributes to default maximum length. :param client: ReportPortal async Client instance to use. If set, all above arguments will be ignored. :param launch_uuid: A launch UUID to use instead of starting own one. @@ -1588,7 +1596,7 @@ def __init__( self.__init_task_list(task_list, task_mutex) self.__last_run_time = datetime.time() self.__init_loop(loop) - if type(launch_uuid) == str: + if type(launch_uuid) is str: super().__init__(endpoint, project, launch_uuid=self.create_task(self.__return_value(launch_uuid)), **kwargs) else: diff --git a/reportportal_client/client.py b/reportportal_client/client.py index 4f6b209..8ea109d 100644 --- a/reportportal_client/client.py +++ b/reportportal_client/client.py @@ -363,6 +363,7 @@ class RPClient(RP): mode: str launch_uuid_print: Optional[bool] print_output: OutputType + truncate_attributes: bool _skip_analytics: str _item_stack: LifoQueue _log_batcher: LogBatcher[RPRequestLog] @@ -433,6 +434,7 @@ def __init__( launch_uuid_print: bool = False, print_output: OutputType = OutputType.STDOUT, log_batcher: Optional[LogBatcher[RPRequestLog]] = None, + truncate_attributes: bool = True, **kwargs: Any ) -> None: """Initialize the class instance with arguments. @@ -455,6 +457,7 @@ def __init__( :param launch_uuid_print: Print Launch UUID into passed TextIO or by default to stdout. :param print_output: Set output stream for Launch UUID printing. :param log_batcher: Use existing LogBatcher instance instead of creation of own one. + :param truncate_attributes: Truncate test item attributes to default maximum length. """ set_current(self) self.api_v1, self.api_v2 = 'v1', 'v2' @@ -490,6 +493,7 @@ def __init__( self._skip_analytics = getenv('AGENT_NO_ANALYTICS') self.launch_uuid_print = launch_uuid_print self.print_output = print_output + self.truncate_attributes = truncate_attributes self.api_key = api_key if not self.api_key: @@ -505,7 +509,7 @@ def __init__( if not self.api_key: warnings.warn( message='Argument `api_key` is `None` or empty string, that is not supposed to happen ' - 'because Report Portal is usually requires an authorization key. Please check ' + 'because ReportPortal is usually requires an authorization key. Please check ' 'your code.', category=RuntimeWarning, stacklevel=2 @@ -538,7 +542,7 @@ def start_launch(self, request_payload = LaunchStartRequest( name=name, start_time=start_time, - attributes=attributes, + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, description=description, mode=self.mode, rerun=rerun, @@ -601,7 +605,7 @@ def start_test_item(self, start_time, item_type, self.launch_uuid, - attributes=verify_value_length(attributes), + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, code_ref=code_ref, description=description, has_stats=has_stats, @@ -655,7 +659,7 @@ def finish_test_item(self, end_time, self.launch_uuid, status, - attributes=attributes, + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, description=description, is_skipped_an_issue=self.is_skipped_an_issue, issue=issue, @@ -691,7 +695,7 @@ def finish_launch(self, request_payload = LaunchFinishRequest( end_time, status=status, - attributes=attributes, + attributes=verify_value_length(attributes) if self.truncate_attributes else attributes, description=kwargs.get('description') ).payload response = HttpRequest(self.session.put, url=url, json=request_payload, @@ -718,7 +722,7 @@ def update_test_item(self, item_uuid: str, attributes: Optional[Union[list, dict """ data = { 'description': description, - 'attributes': verify_value_length(attributes), + 'attributes': verify_value_length(attributes) if self.truncate_attributes else attributes, } item_id = self.get_item_id_by_uuid(item_uuid) url = uri_join(self.base_url_v1, 'item', item_id, 'update') diff --git a/setup.py b/setup.py index 5055f95..3e4dee9 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup, find_packages -__version__ = '5.5.1' +__version__ = '5.5.2' TYPE_STUBS = ['*.pyi'] diff --git a/tests/aio/test_aio_client.py b/tests/aio/test_aio_client.py index bcc264a..c51028d 100644 --- a/tests/aio/test_aio_client.py +++ b/tests/aio/test_aio_client.py @@ -772,3 +772,31 @@ async def test_get_launch_ui_url(aio_client: Client): session.get.assert_called_once() call_args = session.get.call_args_list[0] assert expected_uri == call_args[0][0] + + +@pytest.mark.skipif(sys.version_info < (3, 8), + reason='the test requires AsyncMock which was introduced in Python 3.8') +@pytest.mark.parametrize( + 'method, mock_method, call_method, arguments', + [ + ('start_launch', mock_basic_post_response, 'post', ['Test Launch', timestamp()]), + ('start_test_item', mock_basic_post_response, 'post', ['test_launch_uuid', 'Test Item', timestamp(), + 'SUITE']), + ('finish_test_item', mock_basic_post_response, 'put', ['test_launch_uuid', 'test_item_uuid', + timestamp()]), + ('finish_launch', mock_basic_post_response, 'put', ['test_launch_uuid', timestamp()]), + ('update_test_item', mock_basic_post_response, 'put', ['test_item_uuid']), + ] +) +@pytest.mark.asyncio +async def test_attribute_truncation(aio_client: Client, method, mock_method, call_method, arguments): + # noinspection PyTypeChecker + session: mock.AsyncMock = await aio_client.session() + mock_method(session) + + await getattr(aio_client, method)(*arguments, **{'attributes': {'key': 'value' * 26}}) + getattr(session, call_method).assert_called_once() + kwargs = getattr(session, call_method).call_args_list[0][1] + assert 'attributes' in kwargs['json'] + assert kwargs['json']['attributes'] + assert len(kwargs['json']['attributes'][0]['value']) == 128 diff --git a/tests/test_client.py b/tests/test_client.py index d4a1f3b..83a0ab0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -237,3 +237,27 @@ def test_client_pickling(): pickled_client = pickle.dumps(client) unpickled_client = pickle.loads(pickled_client) assert unpickled_client is not None + + +@pytest.mark.parametrize( + 'method, call_method, arguments', + [ + ('start_launch', 'post', ['Test Launch', timestamp()]), + ('start_test_item', 'post', ['Test Item', timestamp(), 'SUITE']), + ('finish_test_item', 'put', ['test_item_uuid', timestamp()]), + ('finish_launch', 'put', [timestamp()]), + ('update_test_item', 'put', ['test_item_uuid']), + ] +) +def test_attribute_truncation(rp_client: RPClient, method, call_method, arguments): + # noinspection PyTypeChecker + session: mock.Mock = rp_client.session + if method != 'start_launch': + rp_client._RPClient__launch_uuid = 'test_launch_id' + + getattr(rp_client, method)(*arguments, **{'attributes': {'key': 'value' * 26}}) + getattr(session, call_method).assert_called_once() + kwargs = getattr(session, call_method).call_args_list[0][1] + assert 'attributes' in kwargs['json'] + assert kwargs['json']['attributes'] + assert len(kwargs['json']['attributes'][0]['value']) == 128