From 0fed329d434d1e7bb20eddcbbc43a3b4fa218ce7 Mon Sep 17 00:00:00 2001 From: Gguidini Date: Wed, 4 Oct 2023 15:52:32 -0300 Subject: [PATCH] chore: better handle CancelledError from static analysis We have seen stacktraces of static analysis failing with `asyncio.exceptions.CancelledError: Cancelled by cancel scope 7f7514b8ffa0` I do believe that the initial cause is one of the requests failing, and that cancels every other task, but there's a still possible scenario in which the outer future (`asyncio.gather itself`) gets cancelled. Assuming that it happens and that we do get the same exception bubble up, these changes would let the program exit (more) gracefully. Maybe there are other exceptions there that we should also look for? --- .../services/staticanalysis/__init__.py | 9 ++- .../test_static_analysis_service.py | 71 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/codecov_cli/services/staticanalysis/__init__.py b/codecov_cli/services/staticanalysis/__init__.py index 95175ae8..7a9914a5 100644 --- a/codecov_cli/services/staticanalysis/__init__.py +++ b/codecov_cli/services/staticanalysis/__init__.py @@ -109,7 +109,14 @@ async def run_analysis_entrypoint( for el in files_that_need_upload: all_tasks.append(send_single_upload_put(client, all_data, el)) bar.update(1, all_data[el["filepath"]]) - resps = await asyncio.gather(*all_tasks) + try: + resps = await asyncio.gather(*all_tasks) + except asyncio.CancelledError: + message = ( + "Unknown error cancelled the upload tasks.\n" + + f"Uploaded {len(uploaded_files)}/{len(files_that_need_upload)} files successfully." + ) + raise click.ClickException(message) for resp in resps: if resp["succeeded"]: uploaded_files.append(resp["filepath"]) diff --git a/tests/services/static_analysis/test_static_analysis_service.py b/tests/services/static_analysis/test_static_analysis_service.py index fae25226..08876634 100644 --- a/tests/services/static_analysis/test_static_analysis_service.py +++ b/tests/services/static_analysis/test_static_analysis_service.py @@ -1,3 +1,4 @@ +from asyncio import CancelledError from pathlib import Path from unittest.mock import MagicMock @@ -148,6 +149,76 @@ async def side_effect(*args, **kwargs): "raw_upload_location": "http://storage-url", } + @pytest.mark.asyncio + async def test_static_analysis_service_CancelledError(self, mocker): + mock_file_finder = mocker.patch( + "codecov_cli.services.staticanalysis.select_file_finder" + ) + mock_send_upload_put = mocker.patch( + "codecov_cli.services.staticanalysis.send_single_upload_put" + ) + + async def side_effect(client, all_data, el): + if el["filepath"] == "samples/inputs/sample_001.py": + return { + "status_code": 204, + "filepath": el["filepath"], + "succeeded": True, + } + raise CancelledError("Pretending something cancelled this task") + + mock_send_upload_put.side_effect = side_effect + + files_found = map( + lambda filename: FileAnalysisRequest(str(filename), Path(filename)), + [ + "samples/inputs/sample_001.py", + "samples/inputs/sample_002.py", + ], + ) + mock_file_finder.return_value.find_files = MagicMock(return_value=files_found) + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://api.codecov.io/staticanalysis/analyses", + json={ + "external_id": "externalid", + "filepaths": [ + { + "state": "created", + "filepath": "samples/inputs/sample_001.py", + "raw_upload_location": "http://storage-url-001", + }, + { + "state": "created", + "filepath": "samples/inputs/sample_002.py", + "raw_upload_location": "http://storage-url-002", + }, + ], + }, + status=200, + match=[ + matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"}) + ], + ) + + with pytest.raises(click.ClickException) as exp: + await run_analysis_entrypoint( + config={}, + folder=".", + numberprocesses=1, + pattern="*.py", + token="STATIC_TOKEN", + commit="COMMIT", + should_force=False, + folders_to_exclude=[], + enterprise_url=None, + ) + assert "Unknown error cancelled the upload tasks." in str(exp.value) + mock_file_finder.assert_called_with({}) + mock_file_finder.return_value.find_files.assert_called() + assert mock_send_upload_put.call_count == 2 + @pytest.mark.asyncio async def test_send_single_upload_put_success(self, mocker): mock_client = MagicMock()