From 5726dc47078f93057549d22124d5f21063b2f261 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Fri, 28 Jul 2023 15:41:20 -0400 Subject: [PATCH 001/128] Change path_fixes to report_fixes Signed-off-by: joseph-sentry --- codecov_cli/services/upload/upload_sender.py | 2 +- tests/helpers/test_upload_sender.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index af9dc7b5..447aa8e4 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -87,7 +87,7 @@ def _generate_payload( ) -> bytes: network_files = upload_data.network payload = { - "path_fixes": { + "report_fixes": { "format": "legacy", "value": self._get_file_fixers(upload_data), }, diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 26fa5511..75876695 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -237,7 +237,7 @@ def test_generate_payload_overall(self, mocked_coverage_file): get_fake_upload_collection_result(mocked_coverage_file), None ) expected_report = { - "path_fixes": { + "report_fixes": { "format": "legacy", "value": { "SwiftExample/AppDelegate.swift": { @@ -304,7 +304,7 @@ def test_generate_empty_payload_overall(self): UploadCollectionResult([], [], []), None ) expected_report = { - "path_fixes": { + "report_fixes": { "format": "legacy", "value": {}, }, From ce472060e505fc08f74be914011b3eddf2ac404e Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:06:42 -0300 Subject: [PATCH 002/128] feat: surface label-analysis fallbacks (#321) label-analysis was build to be dependable. Even if the calculation fails (which is rare) we have many fallback systems in place, so that you can add the label analysis step before tests and make sure the tests will run. The fallbacks come with a price, though: running all tests. One thing we don't do so well is surfacing when label-analysis fallback in a way customers can use to help build their confidence in label-analysis. We do have logs that point out that something was not successful and that we tried to fallback. Sentry specifically requested that we also surface this information in the dry-run output. These changes to that, letting users check the dry-run output if label-analysis had to fallback, and if so, what was the reason for it. closes codecov/engineering-team#711 --- .github/workflows/ci.yml | 9 + codecov_cli/commands/labelanalysis.py | 38 +++- tests/commands/test_invoke_labelanalysis.py | 206 +++++++++++++++++--- 3 files changed, 219 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6006669..6edb5c81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: submodules: true fetch-depth: 2 - uses: actions/setup-python@v3 + with: + # Because of https://github.com/tree-sitter/py-tree-sitter/issues/162 + python-version: '3.11.x' - name: Install CLI run: | pip install codecov-cli @@ -87,6 +90,9 @@ jobs: submodules: true fetch-depth: 2 - uses: actions/setup-python@v3 + with: + # Because of https://github.com/tree-sitter/py-tree-sitter/issues/162 + python-version: '3.11.x' - name: Install CLI run: | pip install codecov-cli @@ -107,6 +113,9 @@ jobs: submodules: true fetch-depth: 0 - uses: actions/setup-python@v3 + with: + # Because of https://github.com/tree-sitter/py-tree-sitter/issues/162 + python-version: '3.11.x' - name: Install CLI run: | pip install -r requirements.txt -r tests/requirements.txt diff --git a/codecov_cli/commands/labelanalysis.py b/codecov_cli/commands/labelanalysis.py index 5a0e1503..b73009a7 100644 --- a/codecov_cli/commands/labelanalysis.py +++ b/codecov_cli/commands/labelanalysis.py @@ -157,6 +157,7 @@ def label_analysis( runner, dry_run=dry_run, dry_run_format=dry_run_format, + fallback_reason="codecov_unavailable", ) return @@ -189,6 +190,13 @@ def label_analysis( LabelAnalysisRequestResult(request_result), runner, dry_run_format, + # It's possible that the task had processing errors and fallback to all tests + # Even though it's marked as FINISHED (not ERROR) it's not a true success + fallback_reason=( + "test_list_processing_errors" + if resp_json.get("errors", None) + else None + ), ) return if resp_json["state"] == "error": @@ -207,6 +215,7 @@ def label_analysis( runner=runner, dry_run=dry_run, dry_run_format=dry_run_format, + fallback_reason="test_list_processing_failed", ) return if max_wait_time and (time.monotonic() - start_wait) > max_wait_time: @@ -218,6 +227,7 @@ def label_analysis( runner=runner, dry_run=dry_run, dry_run_format=dry_run_format, + fallback_reason="max_wait_time_exceeded", ) return logger.info("Waiting more time for result...") @@ -322,12 +332,16 @@ def _send_labelanalysis_request(payload, url, token_header): def _dry_run_json_output( - labels_to_run: set, labels_to_skip: set, runner_options: List[str] + labels_to_run: set, + labels_to_skip: set, + runner_options: List[str], + fallback_reason: str = None, ) -> None: output_as_dict = dict( runner_options=runner_options, ats_tests_to_run=sorted(labels_to_run), ats_tests_to_skip=sorted(labels_to_skip), + ats_fallback_reason=fallback_reason, ) # ⚠️ DON'T use logger # logger goes to stderr, we want it in stdout @@ -335,8 +349,14 @@ def _dry_run_json_output( def _dry_run_list_output( - labels_to_run: set, labels_to_skip: set, runner_options: List[str] + labels_to_run: set, + labels_to_skip: set, + runner_options: List[str], + fallback_reason: str = None, ) -> None: + if fallback_reason: + logger.warning(f"label-analysis didn't run correctly. Error: {fallback_reason}") + to_run_line = " ".join( sorted(map(lambda l: f"'{l}'", runner_options)) + sorted(map(lambda l: f"'{l}'", labels_to_run)) @@ -355,6 +375,10 @@ def _dry_run_output( result: LabelAnalysisRequestResult, runner: LabelAnalysisRunnerInterface, dry_run_format: str, + *, + # If we have a fallback reason it means that calculating the list of tests to run + # failed at some point. So it was not a completely successful task. + fallback_reason: str = None, ): labels_to_run = set( result.absent_labels + result.global_level_labels + result.present_diff_labels @@ -368,13 +392,16 @@ def _dry_run_output( # Because dry_run_format is a click.Choice we can # be sure the value will be in the dict of choices fn_to_use = format_lookup[dry_run_format] - fn_to_use(labels_to_run, labels_to_skip, runner.dry_run_runner_options) + fn_to_use( + labels_to_run, labels_to_skip, runner.dry_run_runner_options, fallback_reason + ) def _fallback_to_collected_labels( collected_labels: List[str], runner: LabelAnalysisRunnerInterface, *, + fallback_reason: str = None, dry_run: bool = False, dry_run_format: Optional[pathlib.Path] = None, ) -> dict: @@ -393,7 +420,10 @@ def _fallback_to_collected_labels( return runner.process_labelanalysis_result(fake_response) else: return _dry_run_output( - LabelAnalysisRequestResult(fake_response), runner, dry_run_format + LabelAnalysisRequestResult(fake_response), + runner, + dry_run_format, + fallback_reason=fallback_reason, ) logger.error("Cannot fallback to collected labels because no labels were collected") raise click.ClickException("Failed to get list of labels to run") diff --git a/tests/commands/test_invoke_labelanalysis.py b/tests/commands/test_invoke_labelanalysis.py index 22f29925..e18a7f30 100644 --- a/tests/commands/test_invoke_labelanalysis.py +++ b/tests/commands/test_invoke_labelanalysis.py @@ -117,6 +117,7 @@ def test__dry_run_json_output(self): labels_to_run=list_to_run, labels_to_skip=list_to_skip, runner_options=runner_options, + fallback_reason=None, ) stdout = out.getvalue() @@ -124,9 +125,32 @@ def test__dry_run_json_output(self): "runner_options": ["--option=1", "--option=2"], "ats_tests_to_skip": ["label_3", "label_4"], "ats_tests_to_run": ["label_1", "label_2"], + "ats_fallback_reason": None, } - def test__dry_run_json_output(self): + def test__dry_run_json_output_fallback_reason(self): + list_to_run = ["label_1", "label_2", "label_3", "label_4"] + list_to_skip = [] + runner_options = ["--option=1", "--option=2"] + + with StringIO() as out: + with redirect_stdout(out): + _dry_run_json_output( + labels_to_run=list_to_run, + labels_to_skip=list_to_skip, + runner_options=runner_options, + fallback_reason="test_list_processing_errors", + ) + stdout = out.getvalue() + + assert json.loads(stdout) == { + "runner_options": ["--option=1", "--option=2"], + "ats_tests_to_skip": [], + "ats_tests_to_run": ["label_1", "label_2", "label_3", "label_4"], + "ats_fallback_reason": "test_list_processing_errors", + } + + def test__dry_run_space_separated_list_output(self): list_to_run = ["label_1", "label_2"] list_to_skip = ["label_3", "label_4"] runner_options = ["--option=1", "--option=2"] @@ -271,7 +295,10 @@ def test_invoke_label_analysis( ) print(result.output) - def test_invoke_label_analysis_dry_run(self, get_labelanalysis_deps, mocker): + @pytest.mark.parametrize("processing_errors", [[], [{"error": "missing_data"}]]) + def test_invoke_label_analysis_dry_run( + self, processing_errors, get_labelanalysis_deps, mocker + ): mock_get_runner = get_labelanalysis_deps["mock_get_runner"] fake_runner = get_labelanalysis_deps["fake_runner"] @@ -304,7 +331,11 @@ def test_invoke_label_analysis_dry_run(self, get_labelanalysis_deps, mocker): rsps.add( responses.GET, "https://api.codecov.io/labels/labels-analysis/label-analysis-request-id", - json={"state": "finished", "result": label_analysis_result}, + json={ + "state": "finished", + "result": label_analysis_result, + "errors": processing_errors, + }, ) cli_runner = CliRunner(mix_stderr=False) with cli_runner.isolated_filesystem(): @@ -322,10 +353,14 @@ def test_invoke_label_analysis_dry_run(self, get_labelanalysis_deps, mocker): fake_runner.process_labelanalysis_result.assert_not_called() # Dry run format defaults to json print(result.stdout) + ats_fallback_reason = ( + "test_list_processing_errors" if processing_errors else None + ) assert json.loads(result.stdout) == { "runner_options": ["--labels"], "ats_tests_to_run": ["test_absent", "test_global", "test_in_diff"], "ats_tests_to_skip": ["test_present"], + "ats_fallback_reason": ats_fallback_reason, } def test_invoke_label_analysis_dry_run_pytest_format( @@ -444,13 +479,12 @@ def test_fallback_collected_labels_covecov_500_error( print(result.output) assert result.exit_code == 0 - def test_fallback_dry_run(self, get_labelanalysis_deps, mocker, use_verbose_option): + def test_fallback_collected_labels_covecov_500_error_dry_run( + self, get_labelanalysis_deps, mocker + ): mock_get_runner = get_labelanalysis_deps["mock_get_runner"] fake_runner = get_labelanalysis_deps["fake_runner"] collected_labels = get_labelanalysis_deps["collected_labels"] - mock_dry_run = mocker.patch( - "codecov_cli.commands.labelanalysis._dry_run_output" - ) with responses.RequestsMock() as rsps: rsps.add( responses.POST, @@ -460,29 +494,27 @@ def test_fallback_dry_run(self, get_labelanalysis_deps, mocker, use_verbose_opti matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"}) ], ) - cli_runner = CliRunner() - result = cli_runner.invoke( - cli, - [ - "label-analysis", - "--token=STATIC_TOKEN", - f"--base-sha={FAKE_BASE_SHA}", - "--dry-run", - ], - obj={}, - ) - mock_get_runner.assert_called() - fake_runner.process_labelanalysis_result.assert_not_called() - mock_dry_run.assert_called_with( - { - "present_report_labels": [], - "absent_labels": collected_labels, - "present_diff_labels": [], - "global_level_labels": [], - }, - fake_runner, - "json", - ) + cli_runner = CliRunner(mix_stderr=False) + with cli_runner.isolated_filesystem(): + result = cli_runner.invoke( + cli, + [ + "label-analysis", + "--token=STATIC_TOKEN", + f"--base-sha={FAKE_BASE_SHA}", + "--dry-run", + ], + obj={}, + ) + mock_get_runner.assert_called() + fake_runner.process_labelanalysis_result.assert_not_called() + # Dry run format defaults to json + assert json.loads(result.stdout) == { + "runner_options": ["--labels"], + "ats_tests_to_run": sorted(collected_labels), + "ats_tests_to_skip": [], + "ats_fallback_reason": "codecov_unavailable", + } assert result.exit_code == 0 def test_fallback_collected_labels_codecov_error_processing_label_analysis( @@ -544,6 +576,65 @@ def test_fallback_collected_labels_codecov_error_processing_label_analysis( print(result.output) assert result.exit_code == 0 + def test_fallback_collected_labels_codecov_error_processing_label_analysis_dry_run( + self, get_labelanalysis_deps, mocker, use_verbose_option + ): + mock_get_runner = get_labelanalysis_deps["mock_get_runner"] + fake_runner = get_labelanalysis_deps["fake_runner"] + collected_labels = get_labelanalysis_deps["collected_labels"] + + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://api.codecov.io/labels/labels-analysis", + json={"external_id": "label-analysis-request-id"}, + status=201, + match=[ + matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"}) + ], + ) + rsps.add( + responses.PATCH, + "https://api.codecov.io/labels/labels-analysis/label-analysis-request-id", + json={"external_id": "label-analysis-request-id"}, + status=201, + match=[ + matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"}) + ], + ) + rsps.add( + responses.GET, + "https://api.codecov.io/labels/labels-analysis/label-analysis-request-id", + json={ + "state": "error", + "external_id": "uuid4-external-id", + "base_commit": "BASE_COMMIT_SHA", + "head_commit": "HEAD_COMMIT_SHA", + }, + ) + cli_runner = CliRunner(mix_stderr=False) + with cli_runner.isolated_filesystem(): + result = cli_runner.invoke( + cli, + [ + "label-analysis", + "--token=STATIC_TOKEN", + f"--base-sha={FAKE_BASE_SHA}", + "--dry-run", + ], + obj={}, + ) + mock_get_runner.assert_called() + fake_runner.process_labelanalysis_result.assert_not_called() + # Dry run format defaults to json + assert json.loads(result.stdout) == { + "runner_options": ["--labels"], + "ats_tests_to_run": sorted(collected_labels), + "ats_tests_to_skip": [], + "ats_fallback_reason": "test_list_processing_failed", + } + assert result.exit_code == 0 + def test_fallback_collected_labels_codecov_max_wait_time_exceeded( self, get_labelanalysis_deps, mocker, use_verbose_option ): @@ -599,6 +690,61 @@ def test_fallback_collected_labels_codecov_max_wait_time_exceeded( } ) + def test_fallback_collected_labels_codecov_max_wait_time_exceeded_dry_run( + self, get_labelanalysis_deps, mocker, use_verbose_option + ): + mock_get_runner = get_labelanalysis_deps["mock_get_runner"] + fake_runner = get_labelanalysis_deps["fake_runner"] + collected_labels = get_labelanalysis_deps["collected_labels"] + mocker.patch.object(labelanalysis_time, "monotonic", side_effect=[0, 6]) + + with responses.RequestsMock() as rsps: + rsps.add( + responses.POST, + "https://api.codecov.io/labels/labels-analysis", + json={"external_id": "label-analysis-request-id"}, + status=201, + match=[ + matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"}) + ], + ) + rsps.add( + responses.PATCH, + "https://api.codecov.io/labels/labels-analysis/label-analysis-request-id", + json={"external_id": "label-analysis-request-id"}, + status=201, + match=[ + matchers.header_matcher({"Authorization": "Repotoken STATIC_TOKEN"}) + ], + ) + rsps.add( + responses.GET, + "https://api.codecov.io/labels/labels-analysis/label-analysis-request-id", + json={"state": "processing"}, + ) + cli_runner = CliRunner(mix_stderr=False) + result = cli_runner.invoke( + cli, + [ + "label-analysis", + "--token=STATIC_TOKEN", + f"--base-sha={FAKE_BASE_SHA}", + "--max-wait-time=5", + "--dry-run", + ], + obj={}, + ) + mock_get_runner.assert_called() + fake_runner.process_labelanalysis_result.assert_not_called() + # Dry run format defaults to json + assert json.loads(result.stdout) == { + "runner_options": ["--labels"], + "ats_tests_to_run": sorted(collected_labels), + "ats_tests_to_skip": [], + "ats_fallback_reason": "max_wait_time_exceeded", + } + assert result.exit_code == 0 + def test_first_labelanalysis_request_fails_but_second_works( self, get_labelanalysis_deps, mocker, use_verbose_option ): From cfade873052b802571964c0f21d27d8c8d1cd133 Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Tue, 31 Oct 2023 09:45:48 -0300 Subject: [PATCH 003/128] feat: let users specify pytest command (#322) context: codecov/engineering-team#715 Apparently some users don't have a virtualenv or don't install packages globaly. In this case running `python -m pytest` doesn't work because pytest is not accessible to the global python. These changes let users specify a different command to run, so that they can tell the PytestStandardRunner where to find `pytest`. In terms of security it's not more dangerous than having a DAN runner, so should be ok. --- codecov_cli/runners/pytest_standard_runner.py | 15 +++++++++++-- tests/runners/test_pytest_standard_runner.py | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/codecov_cli/runners/pytest_standard_runner.py b/codecov_cli/runners/pytest_standard_runner.py index 29a13010..5a4f692c 100644 --- a/codecov_cli/runners/pytest_standard_runner.py +++ b/codecov_cli/runners/pytest_standard_runner.py @@ -16,6 +16,14 @@ class PytestStandardRunnerConfigParams(dict): + @property + def pytest_command(self) -> List[str]: + command_from_config = self.get("pytest_command") + if isinstance(command_from_config, str): + logger.warning("pytest_command should be a list") + command_from_config = command_from_config.split(" ") + return command_from_config or ["python", "-m", "pytest"] + @property def collect_tests_options(self) -> List[str]: return self.get("collect_tests_options", []) @@ -62,7 +70,7 @@ def _execute_pytest(self, pytest_args: List[str], capture_output: bool = True): Raises Exception if pytest fails Returns the complete pytest output """ - command = ["python", "-m", "pytest"] + pytest_args + command = self.params.pytest_command + pytest_args try: result = subprocess.run( command, @@ -92,7 +100,10 @@ def collect_tests(self): logger.info( "Collecting tests", extra=dict( - extra_log_attributes=dict(pytest_options=options_to_use), + extra_log_attributes=dict( + pytest_command=self.params.pytest_command, + pytest_options=options_to_use, + ), ), ) diff --git a/tests/runners/test_pytest_standard_runner.py b/tests/runners/test_pytest_standard_runner.py index 2ac6780a..ae9866a3 100644 --- a/tests/runners/test_pytest_standard_runner.py +++ b/tests/runners/test_pytest_standard_runner.py @@ -39,6 +39,28 @@ def test_execute_pytest(self, mock_subprocess): ) assert result == output + @pytest.mark.parametrize( + "command_configured", [["pyenv", "pytest"], "pyenv pytest"] + ) + @patch("codecov_cli.runners.pytest_standard_runner.subprocess") + def test_execute_pytest_user_provided_command( + self, mock_subprocess, command_configured + ): + output = "Output in stdout" + return_value = MagicMock(stdout=output.encode("utf-8")) + mock_subprocess.run.return_value = return_value + + runner = PytestStandardRunner(dict(pytest_command=command_configured)) + + result = runner._execute_pytest(["--option", "--ignore=batata"]) + mock_subprocess.run.assert_called_with( + ["pyenv", "pytest", "--option", "--ignore=batata"], + capture_output=True, + check=True, + stdout=None, + ) + assert result == output + @patch("codecov_cli.runners.pytest_standard_runner.subprocess") def test_execute_pytest_fail_collection(self, mock_subprocess): def side_effect(command, *args, **kwargs): From 46761518d368e7f00dc305022e0fa570e360679e Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Tue, 31 Oct 2023 11:46:26 -0400 Subject: [PATCH 004/128] Prepare release 0.4.1 (#323) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e11f15da..7c05c4fd 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.0", + version="0.4.1", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 77468b1a98bed4a3c782803c6eb00ee8a4a8d671 Mon Sep 17 00:00:00 2001 From: Jesse Haka Date: Fri, 20 Oct 2023 11:15:24 +0300 Subject: [PATCH 005/128] Allow JWT tokens --- README.md | 2 +- codecov_cli/commands/base_picking.py | 4 +--- codecov_cli/commands/commit.py | 3 +-- codecov_cli/commands/create_report_result.py | 5 +---- codecov_cli/commands/empty_upload.py | 3 +-- codecov_cli/commands/get_report_results.py | 3 +-- codecov_cli/commands/report.py | 5 +---- codecov_cli/commands/send_notifications.py | 3 +-- codecov_cli/commands/upload.py | 3 +-- codecov_cli/commands/upload_process.py | 3 +-- codecov_cli/helpers/options.py | 1 - codecov_cli/helpers/request.py | 5 +---- codecov_cli/services/commit/__init__.py | 3 +-- codecov_cli/services/report/__init__.py | 8 ++------ codecov_cli/services/upload/__init__.py | 3 +-- codecov_cli/services/upload/legacy_upload_sender.py | 5 +---- codecov_cli/services/upload/upload_sender.py | 3 +-- tests/commands/test_invoke_upload_process.py | 2 +- tests/helpers/test_legacy_upload_sender.py | 3 +-- tests/helpers/test_request.py | 7 ------- tests/helpers/test_upload_sender.py | 3 +-- 21 files changed, 20 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 71f2730a..4b6ffa65 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Codecov-cli supports user input. These inputs, along with their descriptions and | :---: | :---: | :---: | | -C, --sha, --commit-sha TEXT |Commit SHA (with 40 chars) | Required | -r, --slug TEXT |owner/repo slug used instead of the private repo token in Self-hosted | Required -| -t, --token UUID |Codecov upload token | Required +| -t, --token TEXT |Codecov upload token | Required | --git-service | Git provider. Options: github, gitlab, bitbucket, github_enterprise, gitlab_enterprise, bitbucket_server | Optional | -h,--help |Show this message and exit. diff --git a/codecov_cli/commands/base_picking.py b/codecov_cli/commands/base_picking.py index afe0f531..16d70bd9 100644 --- a/codecov_cli/commands/base_picking.py +++ b/codecov_cli/commands/base_picking.py @@ -1,6 +1,5 @@ import logging import typing -import uuid import click @@ -36,7 +35,6 @@ "-t", "--token", help="Codecov upload token", - type=click.UUID, envvar="CODECOV_TOKEN", ) @click.option( @@ -51,7 +49,7 @@ def pr_base_picking( base_sha: str, pr: typing.Optional[int], slug: typing.Optional[str], - token: typing.Optional[uuid.UUID], + token: typing.Optional[str], service: typing.Optional[str], ): enterprise_url = ctx.obj.get("enterprise_url") diff --git a/codecov_cli/commands/commit.py b/codecov_cli/commands/commit.py index 1b2bbb98..15a879d4 100644 --- a/codecov_cli/commands/commit.py +++ b/codecov_cli/commands/commit.py @@ -1,6 +1,5 @@ import logging import typing -import uuid import click @@ -42,7 +41,7 @@ def create_commit( pull_request_number: typing.Optional[int], branch: typing.Optional[str], slug: typing.Optional[str], - token: typing.Optional[uuid.UUID], + token: typing.Optional[str], git_service: typing.Optional[str], fail_on_error: bool, ): diff --git a/codecov_cli/commands/create_report_result.py b/codecov_cli/commands/create_report_result.py index 94bae460..1aa1c21f 100644 --- a/codecov_cli/commands/create_report_result.py +++ b/codecov_cli/commands/create_report_result.py @@ -1,10 +1,7 @@ import logging -import uuid import click -from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum -from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_results_logic @@ -23,7 +20,7 @@ def create_report_results( code: str, slug: str, git_service: str, - token: uuid.UUID, + token: str, fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") diff --git a/codecov_cli/commands/empty_upload.py b/codecov_cli/commands/empty_upload.py index 4c429144..7cdd5428 100644 --- a/codecov_cli/commands/empty_upload.py +++ b/codecov_cli/commands/empty_upload.py @@ -1,6 +1,5 @@ import logging import typing -import uuid import click @@ -19,7 +18,7 @@ def empty_upload( ctx, commit_sha: str, slug: typing.Optional[str], - token: typing.Optional[uuid.UUID], + token: typing.Optional[str], git_service: typing.Optional[str], fail_on_error: typing.Optional[bool], ): diff --git a/codecov_cli/commands/get_report_results.py b/codecov_cli/commands/get_report_results.py index e676afac..a10ccefa 100644 --- a/codecov_cli/commands/get_report_results.py +++ b/codecov_cli/commands/get_report_results.py @@ -1,5 +1,4 @@ import logging -import uuid import click @@ -24,7 +23,7 @@ def get_report_results( code: str, slug: str, git_service: str, - token: uuid.UUID, + token: str, fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") diff --git a/codecov_cli/commands/report.py b/codecov_cli/commands/report.py index 02ea8ec3..6244c228 100644 --- a/codecov_cli/commands/report.py +++ b/codecov_cli/commands/report.py @@ -1,10 +1,7 @@ import logging -import uuid import click -from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum -from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_logic @@ -23,7 +20,7 @@ def create_report( code: str, slug: str, git_service: str, - token: uuid.UUID, + token: str, fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") diff --git a/codecov_cli/commands/send_notifications.py b/codecov_cli/commands/send_notifications.py index ee962cfe..779e4054 100644 --- a/codecov_cli/commands/send_notifications.py +++ b/codecov_cli/commands/send_notifications.py @@ -1,6 +1,5 @@ import logging import typing -import uuid import click @@ -19,7 +18,7 @@ def send_notifications( ctx, commit_sha: str, slug: typing.Optional[str], - token: typing.Optional[uuid.UUID], + token: typing.Optional[str], git_service: typing.Optional[str], fail_on_error: bool, ): diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index cf6a4b1b..9ee04b99 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -2,7 +2,6 @@ import os import pathlib import typing -import uuid import click @@ -185,7 +184,7 @@ def do_upload( coverage_files_search_explicitly_listed_files: typing.List[pathlib.Path], disable_search: bool, disable_file_fixes: bool, - token: typing.Optional[uuid.UUID], + token: typing.Optional[str], plugin_names: typing.List[str], branch: typing.Optional[str], slug: typing.Optional[str], diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index 67fd14da..b8270b79 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -1,7 +1,6 @@ import logging import pathlib import typing -import uuid import click @@ -38,7 +37,7 @@ def upload_process( coverage_files_search_explicitly_listed_files: typing.List[pathlib.Path], disable_search: bool, disable_file_fixes: bool, - token: typing.Optional[uuid.UUID], + token: typing.Optional[str], plugin_names: typing.List[str], branch: typing.Optional[str], slug: typing.Optional[str], diff --git a/codecov_cli/helpers/options.py b/codecov_cli/helpers/options.py index 6b9ceede..ae22c633 100644 --- a/codecov_cli/helpers/options.py +++ b/codecov_cli/helpers/options.py @@ -31,7 +31,6 @@ "-t", "--token", help="Codecov upload token", - type=click.UUID, envvar="CODECOV_TOKEN", ), click.option( diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index 8596e3c7..3cfaa499 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -1,5 +1,4 @@ import logging -import uuid from time import sleep import click @@ -45,13 +44,11 @@ def send_post_request( return request_result(resp) -def get_token_header_or_fail(token: uuid.UUID) -> dict: +def get_token_header_or_fail(token: str) -> dict: if token is None: raise click.ClickException( "Codecov token not found. Please provide Codecov token with -t flag." ) - if not isinstance(token, uuid.UUID): - raise click.ClickException(f"Token must be UUID. Received {type(token)}") return {"Authorization": f"token {token.hex}"} diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index b07117eb..14ded429 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -1,6 +1,5 @@ import logging import typing -import uuid from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug @@ -19,7 +18,7 @@ def create_commit_logic( pr: typing.Optional[str], branch: typing.Optional[str], slug: typing.Optional[str], - token: uuid.UUID, + token: str, service: typing.Optional[str], enterprise_url: typing.Optional[str] = None, fail_on_error: bool = False, diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index d4173ee4..3c58b950 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -1,10 +1,6 @@ import json import logging import time -import typing -import uuid - -import requests from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug @@ -24,7 +20,7 @@ def create_report_logic( code: str, slug: str, service: str, - token: uuid.UUID, + token: str, enterprise_url: str, fail_on_error: bool = False, ): @@ -51,7 +47,7 @@ def create_report_results_logic( code: str, slug: str, service: str, - token: uuid.UUID, + token: str, enterprise_url: str, fail_on_error: bool = False, ): diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index 959d6ed9..c012beb4 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -1,6 +1,5 @@ import logging import typing -import uuid from pathlib import Path import click @@ -39,7 +38,7 @@ def do_upload_logic( coverage_files_search_exclude_folders: typing.List[Path], coverage_files_search_explicitly_listed_files: typing.List[Path], plugin_names: typing.List[str], - token: uuid.UUID, + token: str, branch: typing.Optional[str], slug: typing.Optional[str], pull_request_number: typing.Optional[str], diff --git a/codecov_cli/services/upload/legacy_upload_sender.py b/codecov_cli/services/upload/legacy_upload_sender.py index 99ff6429..53ab4bf4 100644 --- a/codecov_cli/services/upload/legacy_upload_sender.py +++ b/codecov_cli/services/upload/legacy_upload_sender.py @@ -1,10 +1,7 @@ import logging import typing -import uuid from dataclasses import dataclass -import requests - from codecov_cli import __version__ as codecov_cli_version from codecov_cli.helpers.config import LEGACY_CODECOV_API_URL from codecov_cli.helpers.request import send_post_request, send_put_request @@ -39,7 +36,7 @@ def send_upload_data( self, upload_data: UploadCollectionResult, commit_sha: str, - token: uuid.UUID, + token: str, env_vars: typing.Dict[str, str], report_code: str = None, name: typing.Optional[str] = None, diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index af9dc7b5..9c92d577 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -2,7 +2,6 @@ import json import logging import typing -import uuid import zlib from typing import Any, Dict @@ -28,7 +27,7 @@ def send_upload_data( self, upload_data: UploadCollectionResult, commit_sha: str, - token: uuid.UUID, + token: str, env_vars: typing.Dict[str, str], report_code: str, name: typing.Optional[str] = None, diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 026c128d..7476588b 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -70,7 +70,7 @@ def test_upload_process_options(mocker): " -C, --sha, --commit-sha TEXT Commit SHA (with 40 chars) [required]", " -Z, --fail-on-error Exit with non-zero code in case of error", " --git-service [github|gitlab|bitbucket|github_enterprise|gitlab_enterprise|bitbucket_server]", - " -t, --token UUID Codecov upload token", + " -t, --token TEXT Codecov upload token", " -r, --slug TEXT owner/repo slug used instead of the private", " repo token in Self-hosted", " --report-code TEXT The code of the report. If unsure, leave", diff --git a/tests/helpers/test_legacy_upload_sender.py b/tests/helpers/test_legacy_upload_sender.py index 405f87e0..8889d6ec 100644 --- a/tests/helpers/test_legacy_upload_sender.py +++ b/tests/helpers/test_legacy_upload_sender.py @@ -1,4 +1,3 @@ -import uuid from urllib import parse import pytest @@ -11,7 +10,7 @@ from tests.data import reports_examples upload_collection = UploadCollectionResult(["1", "apple.py", "3"], [], []) -random_token = uuid.UUID("f359afb9-8a2a-42ab-a448-c3d267ff495b") +random_token = "f359afb9-8a2a-42ab-a448-c3d267ff495b" random_sha = "845548c6b95223f12e8317a1820705f64beaf69e" named_upload_data = { "name": "name", diff --git a/tests/helpers/test_request.py b/tests/helpers/test_request.py index 4e65f7c5..5b2c0e69 100644 --- a/tests/helpers/test_request.py +++ b/tests/helpers/test_request.py @@ -62,13 +62,6 @@ def test_get_token_header_or_fail(): == "Codecov token not found. Please provide Codecov token with -t flag." ) - # Test with an invalid token type - token = "invalid_token" - with pytest.raises(Exception) as e: - get_token_header_or_fail(token) - - assert str(e.value) == f"Token must be UUID. Received {type(token)}" - def test_request_retry(mocker, valid_response): expected_response = request_result(valid_response) diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 26fa5511..c9943740 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -1,5 +1,4 @@ import json -import uuid from pathlib import Path import pytest @@ -13,7 +12,7 @@ from tests.data import reports_examples upload_collection = UploadCollectionResult(["1", "apple.py", "3"], [], []) -random_token = uuid.UUID("f359afb9-8a2a-42ab-a448-c3d267ff495b") +random_token = "f359afb9-8a2a-42ab-a448-c3d267ff495b" random_sha = "845548c6b95223f12e8317a1820705f64beaf69e" named_upload_data = { "report_code": "report_code", From 68c3f4fec9c00a4b6ebbd65716615ada5b8105cd Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Fri, 3 Nov 2023 10:38:16 -0400 Subject: [PATCH 006/128] Fix tests --- codecov_cli/helpers/request.py | 2 +- codecov_cli/services/report/__init__.py | 2 ++ codecov_cli/services/upload/legacy_upload_sender.py | 2 +- tests/helpers/test_legacy_upload_sender.py | 2 +- tests/helpers/test_request.py | 2 +- tests/helpers/test_upload_sender.py | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index f69eb37e..51060604 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -79,7 +79,7 @@ def get_token_header_or_fail(token: str) -> dict: raise click.ClickException( "Codecov token not found. Please provide Codecov token with -t flag." ) - return {"Authorization": f"token {token.hex}"} + return {"Authorization": f"token {token}"} @retry_request diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 6e94f677..86d1d5a1 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -2,6 +2,8 @@ import logging import time +import requests + from codecov_cli.helpers import request from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug diff --git a/codecov_cli/services/upload/legacy_upload_sender.py b/codecov_cli/services/upload/legacy_upload_sender.py index 53ab4bf4..da91ae7c 100644 --- a/codecov_cli/services/upload/legacy_upload_sender.py +++ b/codecov_cli/services/upload/legacy_upload_sender.py @@ -67,7 +67,7 @@ def send_upload_data( } if token: - headers = {"X-Upload-Token": token.hex} + headers = {"X-Upload-Token": token} else: logger.warning("Token is empty.") headers = {"X-Upload-Token": ""} diff --git a/tests/helpers/test_legacy_upload_sender.py b/tests/helpers/test_legacy_upload_sender.py index 8889d6ec..fdfb9229 100644 --- a/tests/helpers/test_legacy_upload_sender.py +++ b/tests/helpers/test_legacy_upload_sender.py @@ -67,7 +67,7 @@ class TestUploadSender(object): def test_upload_sender_post_called_with_right_parameters( self, mocked_responses, mocked_legacy_upload_endpoint, mocked_storage_server ): - headers = {"X-Upload-Token": random_token.hex} + headers = {"X-Upload-Token": random_token} params = { "package": f"codecov-cli/{codecov_cli_version}", "commit": random_sha, diff --git a/tests/helpers/test_request.py b/tests/helpers/test_request.py index e31c085e..bbd6b531 100644 --- a/tests/helpers/test_request.py +++ b/tests/helpers/test_request.py @@ -57,7 +57,7 @@ def test_get_token_header_or_fail(): # Test with a valid UUID token token = uuid.uuid4() result = get_token_header_or_fail(token) - assert result == {"Authorization": f"token {token.hex}"} + assert result == {"Authorization": f"token {str(token)}"} # Test with a None token token = None diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index f35be6c8..2cc79f85 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -132,7 +132,7 @@ class TestUploadSender(object): def test_upload_sender_post_called_with_right_parameters( self, mocked_responses, mocked_legacy_upload_endpoint, mocked_storage_server ): - headers = {"Authorization": f"token {random_token.hex}"} + headers = {"Authorization": f"token {random_token}"} mocked_legacy_upload_endpoint.match = [ matchers.json_params_matcher(request_data), From a9d261354914eb676ae8d555614bd6b3997a90ea Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Mon, 6 Nov 2023 12:47:31 -0300 Subject: [PATCH 007/128] chore: remove smart-open (#326) We want to add codecov-cli to getsentry/pypi. It was asked that we drop the smart-open dependency. I went back and checked that smart-open is really used for dealing with files in remote storage and decompressing things under-the-hood. We don't need any of that, and are well served by the default open function. So it's fine to drop it. --- codecov_cli/plugins/compress_pycoverage_contexts.py | 1 - requirements.txt | 6 +++--- setup.py | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/codecov_cli/plugins/compress_pycoverage_contexts.py b/codecov_cli/plugins/compress_pycoverage_contexts.py index 5455602f..ab9f8b36 100644 --- a/codecov_cli/plugins/compress_pycoverage_contexts.py +++ b/codecov_cli/plugins/compress_pycoverage_contexts.py @@ -5,7 +5,6 @@ from typing import Any, List import ijson -from smart_open import open from codecov_cli.plugins.types import PreparationPluginReturn diff --git a/requirements.txt b/requirements.txt index 20b98444..9fbe99a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,9 +37,9 @@ requests==2.31.0 responses==0.21.0 # via codecov-cli (setup.py) rfc3986[idna2008]==1.5.0 - # via httpx -smart-open==6.4.0 - # via codecov-cli (setup.py) + # via + # httpx + # rfc3986 sniffio==1.3.0 # via # anyio diff --git a/setup.py b/setup.py index 7c05c4fd..988a916c 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ "ijson==3.*", "pyyaml==6.*", "responses==0.21.*", - "smart-open==6.*", "tree-sitter==0.20.*", ], entry_points={ From 34dea1e3687ab9a5e6ea696981ea23989b8f3273 Mon Sep 17 00:00:00 2001 From: Dana Date: Tue, 7 Nov 2023 13:10:37 +0200 Subject: [PATCH 008/128] check if the PR is a fork PR --- codecov_cli/helpers/encoder.py | 20 +++++ codecov_cli/helpers/git.py | 18 ++++ codecov_cli/helpers/git_services/__init__.py | 0 codecov_cli/helpers/git_services/github.py | 32 +++++++ codecov_cli/services/commit/__init__.py | 7 +- tests/helpers/test_encoder.py | 51 ++++++++++- tests/helpers/test_git.py | 92 ++++++++++++++++++++ 7 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 codecov_cli/helpers/git_services/__init__.py create mode 100644 codecov_cli/helpers/git_services/github.py diff --git a/codecov_cli/helpers/encoder.py b/codecov_cli/helpers/encoder.py index 191e3bfe..167a817e 100644 --- a/codecov_cli/helpers/encoder.py +++ b/codecov_cli/helpers/encoder.py @@ -2,6 +2,7 @@ slug_without_subgroups_regex = re.compile(r"[^/\s]+\/[^/\s]+$") slug_with_subgroups_regex = re.compile(r"[^/\s]+(\/[^/\s]+)+$") +encoded_slug_regex = re.compile(r"[^:\s]+(:::[^:\s]+)*(::::[^:\s]+){1}$") def encode_slug(slug: str): @@ -13,6 +14,16 @@ def encode_slug(slug: str): return encoded_slug +def decode_slug(slug: str): + if slug_encoded_incorrectly(slug): + raise ValueError("The slug is not encoded correctly") + + owner, repo = slug.split("::::", 1) + decoded_owner = "/".join(owner.split(":::")) + decoded_slug = "/".join([decoded_owner, repo]) + return decoded_slug + + def slug_without_subgroups_is_invalid(slug: str): """ Checks if slug is in the form of owner/repo @@ -27,3 +38,12 @@ def slug_with_subgroups_is_invalid(slug: str): Returns True if it's invalid, otherwise return False """ return not slug or not slug_with_subgroups_regex.match(slug) + + +def slug_encoded_incorrectly(slug: str): + """ + Checks if slug is encoded incorrectly based on the encoding mechanism we use. + Checks if slug is in the form of owner:::subowner::::repo or owner::::repo + Returns True if invalid, otherwise returns False + """ + return not slug or not encoded_slug_regex.match(slug) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index 1de19acd..fa614360 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -3,6 +3,9 @@ from enum import Enum from urllib.parse import urlparse +from codecov_cli.helpers.encoder import decode_slug +from codecov_cli.helpers.git_services.github import Github + slug_regex = re.compile(r"[^/\s]+\/[^/\s]+$") logger = logging.getLogger("codecovcli") @@ -17,6 +20,11 @@ class GitService(Enum): BITBUCKET_SERVER = "bitbucket_server" +def get_git_service(git): + if git == "github": + return Github() + + def parse_slug(remote_repo_url: str): """ Extracts a slug from git remote urls. returns None if the url is invalid @@ -82,3 +90,13 @@ def parse_git_service(remote_repo_url: str): extra=dict(remote_repo_url=remote_repo_url), ) return None + + +def is_fork_pr(pr_num, slug, service): + decoded_slug = decode_slug(slug) + git_service = get_git_service(service) + if git_service: + pull_dict = git_service.get_pull_request(decoded_slug, pr_num) + if pull_dict and pull_dict["head"]["slug"] != decoded_slug: + return True + return False diff --git a/codecov_cli/helpers/git_services/__init__.py b/codecov_cli/helpers/git_services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/codecov_cli/helpers/git_services/github.py b/codecov_cli/helpers/git_services/github.py new file mode 100644 index 00000000..e52efe80 --- /dev/null +++ b/codecov_cli/helpers/git_services/github.py @@ -0,0 +1,32 @@ +import json + +import requests + + +class Github: + api_url = "https://api.github.com" + api_version = "2022-11-28" + + def get_pull_request(self, slug, pr_number): + pull_url = f"/repos/{slug}/pulls/{pr_number}" + url = self.api_url + pull_url + headers = {"X-GitHub-Api-Version": self.api_version} + response = requests.get(url, headers=headers) + if response.status_code == 200: + res = json.loads(response.text) + return { + "url": res["url"], + "head": { + "sha": res["head"]["sha"], + "label": res["head"]["label"], + "ref": res["head"]["ref"], + "slug": res["head"]["repo"]["full_name"], + }, + "base": { + "sha": res["base"]["sha"], + "label": res["base"]["label"], + "ref": res["base"]["ref"], + "slug": res["base"]["repo"]["full_name"], + }, + } + return None diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index b07117eb..c50e4a03 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -4,6 +4,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug +from codecov_cli.helpers.git import is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -49,7 +50,11 @@ def send_commit_data( "pullid": pr, "branch": branch, } - headers = get_token_header_or_fail(token) + headers = ( + {} + if not token and is_fork_pr(pr, slug, service) + else get_token_header_or_fail(token) + ) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{slug}/commits" return send_post_request(url=url, data=data, headers=headers) diff --git a/tests/helpers/test_encoder.py b/tests/helpers/test_encoder.py index beb6c1a2..8e422614 100644 --- a/tests/helpers/test_encoder.py +++ b/tests/helpers/test_encoder.py @@ -1,6 +1,11 @@ import pytest -from codecov_cli.helpers.encoder import encode_slug, slug_without_subgroups_is_invalid +from codecov_cli.helpers.encoder import ( + decode_slug, + encode_slug, + slug_encoded_incorrectly, + slug_without_subgroups_is_invalid, +) @pytest.mark.parametrize( @@ -53,3 +58,47 @@ def test_invalid_slug(slug): def test_valid_slug(): slug = "owner/repo" assert not slug_without_subgroups_is_invalid(slug) + + +@pytest.mark.parametrize( + "slug", + [ + ("invalid_slug"), + (""), + (":"), + (":::"), + ("::::"), + ("random string"), + ("owner:::subgroup:::repo"), + ("owner:::repo"), + ("owner::::subgroup::::repo"), + (None), + ], +) +def test_invalid_encoded_slug(slug): + assert slug_encoded_incorrectly(slug) + with pytest.raises(ValueError) as ex: + decode_slug(slug) + + +@pytest.mark.parametrize( + "encoded_slug", + [ + ("owner::::repo"), + ("owner:::subgroup::::repo"), + ], +) +def test_valid_encoded_slug(encoded_slug): + assert not slug_encoded_incorrectly(encoded_slug) + + +@pytest.mark.parametrize( + "encoded_slug, decoded_slug", + [ + ("owner::::repo", "owner/repo"), + ("owner:::subgroup::::repo", "owner/subgroup/repo"), + ], +) +def test_decode_slug(encoded_slug, decoded_slug): + expected_encoded_slug = decode_slug(encoded_slug) + assert expected_encoded_slug == decoded_slug diff --git a/tests/helpers/test_git.py b/tests/helpers/test_git.py index ab3d0407..cd765e74 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -1,6 +1,11 @@ +import json + import pytest +import requests +from requests import Response from codecov_cli.helpers import git +from codecov_cli.helpers.git_services.github import Github @pytest.mark.parametrize( @@ -119,3 +124,90 @@ def test_parse_git_service_valid_address(address, git_service): ) def test_parse_git_service_invalid_service(url): assert git.parse_git_service(url) is None + + +def test_get_git_service_class(): + assert isinstance(git.get_git_service("github"), Github) + assert git.get_git_service("gitlab") == None + assert git.get_git_service("bitbucket") == None + + +def test_pr_is_not_fork_pr(mocker): + def mock_request(*args, headers={}, **kwargs): + assert headers["X-GitHub-Api-Version"] == "2022-11-28" + res = { + "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", + "head": { + "sha": "123", + "label": "codecov-cli:branch", + "ref": "branch", + "repo": {"full_name": "codecov/codecov-cli"}, + }, + "base": { + "sha": "123", + "label": "codecov-cli:main", + "ref": "main", + "repo": {"full_name": "codecov/codecov-cli"}, + }, + } + response = Response() + response.status_code = 200 + response._content = json.dumps(res).encode("utf-8") + return response + + mocker.patch.object( + requests, + "get", + side_effect=mock_request, + ) + encoded_slug = "codecov::::codecov-cli" + assert not git.is_fork_pr(1, encoded_slug, "github") + + +def test_pr_is_fork_pr(mocker): + def mock_request(*args, headers={}, **kwargs): + assert headers["X-GitHub-Api-Version"] == "2022-11-28" + res = { + "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/325", + "head": { + "sha": "123", + "label": "codecov-cli:branch", + "ref": "branch", + "repo": {"full_name": "user_forked_repo/codecov-cli"}, + }, + "base": { + "sha": "123", + "label": "codecov-cli:main", + "ref": "main", + "repo": {"full_name": "codecov/codecov-cli"}, + }, + } + response = Response() + response.status_code = 200 + response._content = json.dumps(res).encode("utf-8") + return response + + mocker.patch.object( + requests, + "get", + side_effect=mock_request, + ) + encoded_slug = "codecov::::codecov-cli" + assert git.is_fork_pr(1, encoded_slug, "github") + + +def test_pr_not_found(mocker): + def mock_request(*args, headers={}, **kwargs): + assert headers["X-GitHub-Api-Version"] == "2022-11-28" + response = Response() + response.status_code = 404 + response._content = b"not-found" + return response + + mocker.patch.object( + requests, + "get", + side_effect=mock_request, + ) + encoded_slug = "codecov::::codecov-cli" + assert not git.is_fork_pr(1, encoded_slug, "github") From 85d7c6704e7466a9c74e92ea62ffe8b10557e2c7 Mon Sep 17 00:00:00 2001 From: Dana Date: Wed, 8 Nov 2023 13:00:56 +0200 Subject: [PATCH 009/128] adding tests --- codecov_cli/helpers/git.py | 2 +- tests/helpers/git_services/test_github.py | 73 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/helpers/git_services/test_github.py diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index fa614360..e53a5bde 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -97,6 +97,6 @@ def is_fork_pr(pr_num, slug, service): git_service = get_git_service(service) if git_service: pull_dict = git_service.get_pull_request(decoded_slug, pr_num) - if pull_dict and pull_dict["head"]["slug"] != decoded_slug: + if pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"]: return True return False diff --git a/tests/helpers/git_services/test_github.py b/tests/helpers/git_services/test_github.py new file mode 100644 index 00000000..c05a27af --- /dev/null +++ b/tests/helpers/git_services/test_github.py @@ -0,0 +1,73 @@ +import json + +import pytest +import requests +from requests import Response + +from codecov_cli.helpers import git +from codecov_cli.helpers.git_services.github import Github + + +def test_get_pull_request(mocker): + def mock_request(*args, headers={}, **kwargs): + assert headers["X-GitHub-Api-Version"] == "2022-11-28" + res = { + "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", + "head": { + "sha": "123", + "label": "codecov-cli:branch", + "ref": "branch", + "repo": {"full_name": "user_forked_repo/codecov-cli"}, + }, + "base": { + "sha": "123", + "label": "codecov-cli:main", + "ref": "main", + "repo": {"full_name": "codecov/codecov-cli"}, + }, + } + response = Response() + response.status_code = 200 + response._content = json.dumps(res).encode("utf-8") + return response + + mocker.patch.object( + requests, + "get", + side_effect=mock_request, + ) + slug = "codecov/codecov-cli" + response = Github().get_pull_request(slug, 1) + assert response == { + "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", + "head": { + "sha": "123", + "label": "codecov-cli:branch", + "ref": "branch", + "slug": "user_forked_repo/codecov-cli", + }, + "base": { + "sha": "123", + "label": "codecov-cli:main", + "ref": "main", + "slug": "codecov/codecov-cli", + }, + } + +def test_get_pull_request_404(mocker): + def mock_request(*args, headers={}, **kwargs): + assert headers["X-GitHub-Api-Version"] == "2022-11-28" + res = {} + response = Response() + response.status_code = 404 + response._content = json.dumps(res).encode("utf-8") + return response + + mocker.patch.object( + requests, + "get", + side_effect=mock_request, + ) + slug = "codecov/codecov-cli" + response = Github().get_pull_request(slug, 1) + assert response == None From 0dfd7172b87b32b4d546c89338271f4f78caba7f Mon Sep 17 00:00:00 2001 From: Dana Date: Wed, 8 Nov 2023 13:23:37 +0200 Subject: [PATCH 010/128] taking decoding slug out of is_fork_pr, cause it doesn't feel right to be there --- codecov_cli/helpers/git.py | 7 +++++-- codecov_cli/services/commit/__init__.py | 5 +++-- codecov_cli/services/upload/upload_sender.py | 7 ++++++- tests/services/commit/test_commit_service.py | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index e53a5bde..34f4fdfc 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -93,10 +93,13 @@ def parse_git_service(remote_repo_url: str): def is_fork_pr(pr_num, slug, service): - decoded_slug = decode_slug(slug) + """ + takes in pull request number, slug in the owner/repo format, and the git service e.g. github, gitlab etc. + returns true if PR is made in a fork context, false if not. + """ git_service = get_git_service(service) if git_service: - pull_dict = git_service.get_pull_request(decoded_slug, pr_num) + pull_dict = git_service.get_pull_request(slug, pr_num) if pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"]: return True return False diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index c50e4a03..c979fab3 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -3,7 +3,7 @@ import uuid from codecov_cli.helpers.config import CODECOV_API_URL -from codecov_cli.helpers.encoder import encode_slug +from codecov_cli.helpers.encoder import decode_slug, encode_slug from codecov_cli.helpers.git import is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, @@ -50,9 +50,10 @@ def send_commit_data( "pullid": pr, "branch": branch, } + decoded_slug = decode_slug(slug) headers = ( {} - if not token and is_fork_pr(pr, slug, service) + if not token and is_fork_pr(pr, decoded_slug, service) else get_token_header_or_fail(token) ) upload_url = enterprise_url or CODECOV_API_URL diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index 447aa8e4..ccec92f4 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -9,6 +9,7 @@ from codecov_cli import __version__ as codecov_cli_version from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug +from codecov_cli.helpers.git import is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, send_post_request, @@ -53,7 +54,11 @@ def send_upload_data( } # Data to upload to Codecov - headers = get_token_header_or_fail(token) + headers = ( + {} + if not token and is_fork_pr(pull_request_number, slug, git_service) + else get_token_header_or_fail(token) + ) encoded_slug = encode_slug(slug) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/uploads" diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 082b2ab8..44cbebb9 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -103,7 +103,7 @@ def test_commit_sender_200(mocker): ) token = uuid.uuid4() res = send_commit_data( - "commit_sha", "parent_sha", "pr", "branch", "slug", token, "service", None + "commit_sha", "parent_sha", "pr", "branch", "owner::::repo", token, "service", None ) assert res.error is None assert res.warnings == [] @@ -117,7 +117,7 @@ def test_commit_sender_403(mocker): ) token = uuid.uuid4() res = send_commit_data( - "commit_sha", "parent_sha", "pr", "branch", "slug", token, "service", None + "commit_sha", "parent_sha", "pr", "branch", "owner::::repo", token, "service", None ) assert res.error == RequestError( code="HTTP Error 403", From 5ed826171e8e20920104397352b9a6d610a02f1f Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:04:54 -0300 Subject: [PATCH 011/128] chore: Fix helper text in label analysis (#315) Since the previous change to label analysis the help text was outdated. These changes fix it to be more accurate. Also removing the test for the help text because it's more annoying than it's helpful. It doesn't really _test_ anything, because the formatting is done by click --- codecov_cli/commands/labelanalysis.py | 8 ++---- tests/commands/test_invoke_labelanalysis.py | 32 --------------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/codecov_cli/commands/labelanalysis.py b/codecov_cli/commands/labelanalysis.py index b73009a7..5298854b 100644 --- a/codecov_cli/commands/labelanalysis.py +++ b/codecov_cli/commands/labelanalysis.py @@ -58,11 +58,8 @@ "--dry-run", "dry_run", help=( - "Print list of tests to run AND tests skipped (and options that need to be added to the test runner) to stdout. " - + "Also prints the same information in JSON format. " - + "JSON will have keys 'ats_tests_to_run', 'ats_tests_to_skip' and 'runner_options'. " - + "List of tests to run is prefixed with ATS_TESTS_TO_RUN= " - + "List of tests to skip is prefixed with ATS_TESTS_TO_SKIP=" + "Print list of tests to run AND tests skipped AND options that need to be added to the test runner to stdout. " + + "Choose format with --dry-run-format option. Default is JSON. " ), is_flag=True, ) @@ -70,6 +67,7 @@ "--dry-run-format", "dry_run_format", type=click.Choice(["json", "space-separated-list"]), + help="Format in which --dry-run data is printed. Default is JSON.", default="json", ) @click.pass_context diff --git a/tests/commands/test_invoke_labelanalysis.py b/tests/commands/test_invoke_labelanalysis.py index e18a7f30..8cbe3fe9 100644 --- a/tests/commands/test_invoke_labelanalysis.py +++ b/tests/commands/test_invoke_labelanalysis.py @@ -171,38 +171,6 @@ def test__dry_run_space_separated_list_output(self): class TestLabelAnalysisCommand(object): - def test_labelanalysis_help(self, mocker, fake_ci_provider): - mocker.patch("codecov_cli.main.get_ci_adapter", return_value=fake_ci_provider) - runner = CliRunner() - - result = runner.invoke(cli, ["label-analysis", "--help"], obj={}) - assert result.exit_code == 0 - print(result.output) - assert result.output.split("\n") == [ - "Usage: cli label-analysis [OPTIONS]", - "", - "Options:", - " --token TEXT The static analysis token (NOT the same token", - " as upload) [required]", - " --head-sha TEXT Commit SHA (with 40 chars) [required]", - " --base-sha TEXT Commit SHA (with 40 chars) [required]", - " --runner-name, --runner TEXT Runner to use", - " --max-wait-time INTEGER Max time (in seconds) to wait for the label", - " analysis result before falling back to running", - " all tests. Default is to wait forever.", - " --dry-run Print list of tests to run AND tests skipped", - " (and options that need to be added to the test", - " runner) to stdout. Also prints the same", - " information in JSON format. JSON will have", - " keys 'ats_tests_to_run', 'ats_tests_to_skip'", - " and 'runner_options'. List of tests to run is", - " prefixed with ATS_TESTS_TO_RUN= List of tests", - " to skip is prefixed with ATS_TESTS_TO_SKIP=", - " --dry-run-format [json|space-separated-list]", - " -h, --help Show this message and exit.", - "", - ] - def test_invoke_label_analysis_missing_token(self, mocker, fake_ci_provider): mocker.patch("codecov_cli.main.get_ci_adapter", return_value=fake_ci_provider) runner = CliRunner() From b190aa8f73fb64d90a7315c1b3c43edd812e957b Mon Sep 17 00:00:00 2001 From: Dana Date: Wed, 8 Nov 2023 15:15:08 +0200 Subject: [PATCH 012/128] we need the pr number for the PR API request to know if the command runs in a fork PR --- codecov_cli/commands/report.py | 19 ++++++++++++++++- codecov_cli/services/report/__init__.py | 22 +++++++++++++++----- tests/helpers/git_services/test_github.py | 1 + tests/services/commit/test_commit_service.py | 18 ++++++++++++++-- tests/services/report/test_report_service.py | 16 ++++++++++---- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/codecov_cli/commands/report.py b/codecov_cli/commands/report.py index 02ea8ec3..92a44aa6 100644 --- a/codecov_cli/commands/report.py +++ b/codecov_cli/commands/report.py @@ -15,6 +15,15 @@ @click.option( "--code", help="The code of the report. If unsure, leave default", default="default" ) +@click.option( + "-P", + "--pr", + "--pull-request-number", + "pull_request_number", + help="Specify the pull request number mannually. Used to override pre-existing CI environment variables", + cls=CodecovOption, + fallback_field=FallbackFieldEnum.pull_request_number, +) @global_options @click.pass_context def create_report( @@ -25,6 +34,7 @@ def create_report( git_service: str, token: uuid.UUID, fail_on_error: bool, + pull_request_number: int, ): enterprise_url = ctx.obj.get("enterprise_url") logger.debug( @@ -41,7 +51,14 @@ def create_report( ), ) res = create_report_logic( - commit_sha, code, slug, git_service, token, enterprise_url, fail_on_error + commit_sha, + code, + slug, + git_service, + token, + enterprise_url, + pull_request_number, + fail_on_error, ) if not res.error: logger.info( diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 4d57d66d..0e159b40 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -1,14 +1,14 @@ import json import logging import time -import typing import uuid import requests from codecov_cli.helpers import request from codecov_cli.helpers.config import CODECOV_API_URL -from codecov_cli.helpers.encoder import encode_slug +from codecov_cli.helpers.encoder import decode_slug, encode_slug +from codecov_cli.helpers.git import is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -27,21 +27,33 @@ def create_report_logic( service: str, token: uuid.UUID, enterprise_url: str, + pull_request_number: int, fail_on_error: bool = False, ): encoded_slug = encode_slug(slug) sending_result = send_create_report_request( - commit_sha, code, service, token, encoded_slug, enterprise_url + commit_sha, + code, + service, + token, + encoded_slug, + enterprise_url, + pull_request_number, ) log_warnings_and_errors_if_any(sending_result, "Report creating", fail_on_error) return sending_result def send_create_report_request( - commit_sha, code, service, token, encoded_slug, enterprise_url + commit_sha, code, service, token, encoded_slug, enterprise_url, pull_request_number ): data = {"code": code} - headers = get_token_header_or_fail(token) + decoded_slug = decode_slug(encoded_slug) + headers = ( + {} + if not token and is_fork_pr(pull_request_number, decoded_slug, service) + else get_token_header_or_fail(token) + ) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" return send_post_request(url=url, headers=headers, data=data) diff --git a/tests/helpers/git_services/test_github.py b/tests/helpers/git_services/test_github.py index c05a27af..05662766 100644 --- a/tests/helpers/git_services/test_github.py +++ b/tests/helpers/git_services/test_github.py @@ -54,6 +54,7 @@ def mock_request(*args, headers={}, **kwargs): }, } + def test_get_pull_request_404(mocker): def mock_request(*args, headers={}, **kwargs): assert headers["X-GitHub-Api-Version"] == "2022-11-28" diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 44cbebb9..f23bcafc 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -103,7 +103,14 @@ def test_commit_sender_200(mocker): ) token = uuid.uuid4() res = send_commit_data( - "commit_sha", "parent_sha", "pr", "branch", "owner::::repo", token, "service", None + "commit_sha", + "parent_sha", + "pr", + "branch", + "owner::::repo", + token, + "service", + None, ) assert res.error is None assert res.warnings == [] @@ -117,7 +124,14 @@ def test_commit_sender_403(mocker): ) token = uuid.uuid4() res = send_commit_data( - "commit_sha", "parent_sha", "pr", "branch", "owner::::repo", token, "service", None + "commit_sha", + "parent_sha", + "pr", + "branch", + "owner::::repo", + token, + "service", + None, ) assert res.error == RequestError( code="HTTP Error 403", diff --git a/tests/services/report/test_report_service.py b/tests/services/report/test_report_service.py index 2a4dc2e8..8d31a112 100644 --- a/tests/services/report/test_report_service.py +++ b/tests/services/report/test_report_service.py @@ -13,7 +13,13 @@ def test_send_create_report_request_200(mocker): return_value=mocker.MagicMock(status_code=200), ) res = send_create_report_request( - "commit_sha", "code", "github", uuid.uuid4(), "slug", "enterprise_url" + "commit_sha", + "code", + "github", + uuid.uuid4(), + "owner::::repo", + "enterprise_url", + 1, ) assert res.error is None assert res.warnings == [] @@ -26,7 +32,7 @@ def test_send_create_report_request_403(mocker): return_value=mocker.MagicMock(status_code=403, text="Permission denied"), ) res = send_create_report_request( - "commit_sha", "code", "github", uuid.uuid4(), "slug", None + "commit_sha", "code", "github", uuid.uuid4(), "owner::::repo", None, 1 ) assert res.error == RequestError( code="HTTP Error 403", @@ -55,6 +61,7 @@ def test_create_report_command_with_warnings(mocker): service="github", token="token", enterprise_url=None, + pull_request_number=1, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) @@ -70,7 +77,7 @@ def test_create_report_command_with_warnings(mocker): text="", ) mocked_send_request.assert_called_with( - "commit_sha", "code", "github", "token", "owner::::repo", None + "commit_sha", "code", "github", "token", "owner::::repo", None, 1 ) @@ -96,6 +103,7 @@ def test_create_report_command_with_error(mocker): slug="owner/repo", service="github", token="token", + pull_request_number=1, enterprise_url="enterprise_url", ) @@ -115,5 +123,5 @@ def test_create_report_command_with_error(mocker): warnings=[], ) mock_send_report_data.assert_called_with( - "commit_sha", "code", "github", "token", "owner::::repo", "enterprise_url" + "commit_sha", "code", "github", "token", "owner::::repo", "enterprise_url", 1 ) From e313d722de1bdbe29ad19e79761caf2bbffd9683 Mon Sep 17 00:00:00 2001 From: Dana Date: Fri, 10 Nov 2023 13:16:50 +0200 Subject: [PATCH 013/128] amend branch names to be forked-slug:branch-name --- codecov_cli/helpers/git.py | 20 +++++-- codecov_cli/services/commit/__init__.py | 18 +++--- codecov_cli/services/report/__init__.py | 9 +-- codecov_cli/services/upload/upload_sender.py | 7 ++- tests/helpers/test_git.py | 14 ++--- tests/services/commit/test_commit_service.py | 58 ++++++++++++++++++++ 6 files changed, 101 insertions(+), 25 deletions(-) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index 34f4fdfc..9aa6016c 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -92,14 +92,24 @@ def parse_git_service(remote_repo_url: str): return None -def is_fork_pr(pr_num, slug, service): +def is_fork_pr(pull_dict): """ - takes in pull request number, slug in the owner/repo format, and the git service e.g. github, gitlab etc. + takes in dict: pull_dict returns true if PR is made in a fork context, false if not. """ + return ( + True + if pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"] + else False + ) + + +def get_pull(service, slug, pr_num): + """ + takes in str git service e.g. github, gitlab etc., slug in the owner/repo format, and the pull request number + returns the pull request info gotten from the git service provider if successful, None if not + """ git_service = get_git_service(service) if git_service: pull_dict = git_service.get_pull_request(slug, pr_num) - if pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"]: - return True - return False + return pull_dict diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index c979fab3..6621bd98 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -4,7 +4,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug -from codecov_cli.helpers.git import is_fork_pr +from codecov_cli.helpers.git import get_git_service, get_pull, is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -44,18 +44,22 @@ def create_commit_logic( def send_commit_data( commit_sha, parent_sha, pr, branch, slug, token, service, enterprise_url ): + decoded_slug = decode_slug(slug) + pull_dict = get_pull(service, decoded_slug, pr) if not token else None + if is_fork_pr(pull_dict): + headers = {} + branch = pull_dict["head"]["slug"] + ":" + branch + logger.info("The PR is happening in a forked repo. Using tokenless upload.") + else: + headers = get_token_header_or_fail(token) + data = { "commitid": commit_sha, "parent_commit_id": parent_sha, "pullid": pr, "branch": branch, } - decoded_slug = decode_slug(slug) - headers = ( - {} - if not token and is_fork_pr(pr, decoded_slug, service) - else get_token_header_or_fail(token) - ) + upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{slug}/commits" return send_post_request(url=url, data=data, headers=headers) diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 0e159b40..e8a62385 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -8,7 +8,7 @@ from codecov_cli.helpers import request from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug -from codecov_cli.helpers.git import is_fork_pr +from codecov_cli.helpers.git import get_pull, is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -49,10 +49,11 @@ def send_create_report_request( ): data = {"code": code} decoded_slug = decode_slug(encoded_slug) + pull_dict = ( + get_pull(service, decoded_slug, pull_request_number) if not token else None + ) headers = ( - {} - if not token and is_fork_pr(pull_request_number, decoded_slug, service) - else get_token_header_or_fail(token) + {} if not token and is_fork_pr(pull_dict) else get_token_header_or_fail(token) ) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index ccec92f4..c9ede54b 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -9,7 +9,7 @@ from codecov_cli import __version__ as codecov_cli_version from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug -from codecov_cli.helpers.git import is_fork_pr +from codecov_cli.helpers.git import get_pull, is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, send_post_request, @@ -54,9 +54,12 @@ def send_upload_data( } # Data to upload to Codecov + pull_dict = ( + get_pull(git_service, slug, pull_request_number) if not token else None + ) headers = ( {} - if not token and is_fork_pr(pull_request_number, slug, git_service) + if not token and is_fork_pr(pull_dict) else get_token_header_or_fail(token) ) encoded_slug = encode_slug(slug) diff --git a/tests/helpers/test_git.py b/tests/helpers/test_git.py index cd765e74..155a33c2 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -160,15 +160,15 @@ def mock_request(*args, headers={}, **kwargs): "get", side_effect=mock_request, ) - encoded_slug = "codecov::::codecov-cli" - assert not git.is_fork_pr(1, encoded_slug, "github") + pull_dict = git.get_pull("github", "codecov/codecov-cli", 1) + assert not git.is_fork_pr(pull_dict) def test_pr_is_fork_pr(mocker): def mock_request(*args, headers={}, **kwargs): assert headers["X-GitHub-Api-Version"] == "2022-11-28" res = { - "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/325", + "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", "head": { "sha": "123", "label": "codecov-cli:branch", @@ -192,8 +192,8 @@ def mock_request(*args, headers={}, **kwargs): "get", side_effect=mock_request, ) - encoded_slug = "codecov::::codecov-cli" - assert git.is_fork_pr(1, encoded_slug, "github") + pull_dict = git.get_pull("github", "codecov/codecov-cli", 1) + assert git.is_fork_pr(pull_dict) def test_pr_not_found(mocker): @@ -209,5 +209,5 @@ def mock_request(*args, headers={}, **kwargs): "get", side_effect=mock_request, ) - encoded_slug = "codecov::::codecov-cli" - assert not git.is_fork_pr(1, encoded_slug, "github") + pull_dict = git.get_pull("github", "codecov/codecov-cli", 1) + assert not git.is_fork_pr(pull_dict) diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index f23bcafc..2aeff136 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -1,6 +1,9 @@ +import json import uuid +import requests from click.testing import CliRunner +from requests import Response from codecov_cli.services.commit import create_commit_logic, send_commit_data from codecov_cli.types import RequestError, RequestResult, RequestResultWarning @@ -139,3 +142,58 @@ def test_commit_sender_403(mocker): params={}, ) mocked_response.assert_called_once() + + +def test_commit_sender_with_forked_repo(mocker): + mocked_response = mocker.patch( + "codecov_cli.services.commit.send_post_request", + return_value=mocker.MagicMock(status_code=200, text="success"), + ) + + def mock_request(*args, headers={}, **kwargs): + assert headers["X-GitHub-Api-Version"] == "2022-11-28" + res = { + "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", + "head": { + "sha": "123", + "label": "codecov-cli:branch", + "ref": "branch", + "repo": {"full_name": "user_forked_repo/codecov-cli"}, + }, + "base": { + "sha": "123", + "label": "codecov-cli:main", + "ref": "main", + "repo": {"full_name": "codecov/codecov-cli"}, + }, + } + response = Response() + response.status_code = 200 + response._content = json.dumps(res).encode("utf-8") + return response + + mocker.patch.object( + requests, + "get", + side_effect=mock_request, + ) + res = send_commit_data( + "commit_sha", + "parent_sha", + "1", + "branch", + "codecov::::codecov-cli", + None, + "github", + None, + ) + mocked_response.assert_called_with( + url="https://api.codecov.io/upload/github/codecov::::codecov-cli/commits", + data={ + "commitid": "commit_sha", + "parent_commit_id": "parent_sha", + "pullid": "1", + "branch": "user_forked_repo/codecov-cli:branch", + }, + headers={}, + ) From b60718f58e5efc7450137aa0c4c513b6739bee35 Mon Sep 17 00:00:00 2001 From: Dana Date: Tue, 14 Nov 2023 12:16:45 +0200 Subject: [PATCH 014/128] simplify the ternary operator --- codecov_cli/helpers/git.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index 9aa6016c..5c79ae6e 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -97,11 +97,7 @@ def is_fork_pr(pull_dict): takes in dict: pull_dict returns true if PR is made in a fork context, false if not. """ - return ( - True - if pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"] - else False - ) + return pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"] def get_pull(service, slug, pr_num): From 29febe6b8c51592121c34eed35d9cdff42be034d Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Mon, 6 Nov 2023 18:20:15 -0500 Subject: [PATCH 015/128] Fix lineno being unset If lineno is unset that means that the file is empty thus the eof should be 0, so lineno will be -1. Signed-off-by: joseph-sentry --- codecov_cli/services/upload/upload_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index d30e036e..5e362e91 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -112,6 +112,7 @@ def _get_file_fixes( try: with open(filename, "r") as f: + lineno = -1 for lineno, line_content in enumerate(f): if any( pattern.match(line_content) @@ -123,7 +124,6 @@ def _get_file_fixes( for pattern in fix_patterns_to_apply.without_reason ): fixed_lines_without_reason.add(lineno + 1) - if fix_patterns_to_apply.eof: eof = lineno + 1 except UnicodeDecodeError as err: From 8190ab7ac83b6635df19bb8e31139732a189c181 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:36:21 -0500 Subject: [PATCH 016/128] Add comment explaining lineno change --- codecov_cli/services/upload/upload_collector.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 5e362e91..8dbf3ce8 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -112,6 +112,9 @@ def _get_file_fixes( try: with open(filename, "r") as f: + # If lineno is unset that means that the + # file is empty thus the eof should be 0 + # so lineno will be set to -1 here lineno = -1 for lineno, line_content in enumerate(f): if any( From ae8d4b0a3ec945e44d0207acec6de18dfb12cebe Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:05:26 -0500 Subject: [PATCH 017/128] add another comment below lineno --- codecov_cli/services/upload/upload_collector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 8dbf3ce8..0668d9d6 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -116,6 +116,8 @@ def _get_file_fixes( # file is empty thus the eof should be 0 # so lineno will be set to -1 here lineno = -1 + # overwrite lineno in this for loop + # if f is empty, lineno stays at -1 for lineno, line_content in enumerate(f): if any( pattern.match(line_content) From bb74d7f205a5f953e6bce69059961cc5d99bac56 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Wed, 8 Nov 2023 15:44:51 -0500 Subject: [PATCH 018/128] Change thread pool method in process_files Signed-off-by: joseph-sentry --- codecov_cli/services/staticanalysis/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/codecov_cli/services/staticanalysis/__init__.py b/codecov_cli/services/staticanalysis/__init__.py index 55a651fd..171acd8a 100644 --- a/codecov_cli/services/staticanalysis/__init__.py +++ b/codecov_cli/services/staticanalysis/__init__.py @@ -5,6 +5,7 @@ from functools import partial from multiprocessing import get_context from pathlib import Path +import sys import click import httpx @@ -183,11 +184,15 @@ async def process_files( all_data = {} file_metadata = [] errors = {} + if sys.platform.startswith("win32") or sys.platform.startswith("darwin"): + pool_thread_method = "spawn" + else: + pool_thread_method = "fork" with click.progressbar( length=len(files_to_analyze), label="Analyzing files", ) as bar: - with get_context("fork").Pool(processes=numberprocesses) as pool: + with get_context(pool_thread_method).Pool(processes=numberprocesses) as pool: file_results = pool.imap_unordered(mapped_func, files_to_analyze) for result in file_results: bar.update(1, result) From 3f6658581f94920877faaada117693a5bcf6ef20 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Wed, 8 Nov 2023 15:51:19 -0500 Subject: [PATCH 019/128] make lint Signed-off-by: joseph-sentry --- codecov_cli/services/staticanalysis/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov_cli/services/staticanalysis/__init__.py b/codecov_cli/services/staticanalysis/__init__.py index 171acd8a..163de67f 100644 --- a/codecov_cli/services/staticanalysis/__init__.py +++ b/codecov_cli/services/staticanalysis/__init__.py @@ -1,11 +1,11 @@ import asyncio import json import logging +import sys import typing from functools import partial from multiprocessing import get_context from pathlib import Path -import sys import click import httpx From 832d641fde57179878fe0960d4037ee11e285b93 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:31:17 -0500 Subject: [PATCH 020/128] fix: use default pool method and add comment --- codecov_cli/services/staticanalysis/__init__.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/codecov_cli/services/staticanalysis/__init__.py b/codecov_cli/services/staticanalysis/__init__.py index 163de67f..f82457df 100644 --- a/codecov_cli/services/staticanalysis/__init__.py +++ b/codecov_cli/services/staticanalysis/__init__.py @@ -1,10 +1,9 @@ import asyncio import json import logging -import sys import typing from functools import partial -from multiprocessing import get_context +from multiprocessing import Pool from pathlib import Path import click @@ -184,15 +183,13 @@ async def process_files( all_data = {} file_metadata = [] errors = {} - if sys.platform.startswith("win32") or sys.platform.startswith("darwin"): - pool_thread_method = "spawn" - else: - pool_thread_method = "fork" with click.progressbar( length=len(files_to_analyze), label="Analyzing files", ) as bar: - with get_context(pool_thread_method).Pool(processes=numberprocesses) as pool: + # https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods + # from the link above, we want to use the default start methods + with Pool(processes=numberprocesses) as pool: file_results = pool.imap_unordered(mapped_func, files_to_analyze) for result in file_results: bar.update(1, result) From e523a845a48bfa1ae380c47f5bd628e79213e2a1 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Fri, 24 Nov 2023 12:39:06 -0500 Subject: [PATCH 021/128] test(static-analysis): remove get_context mock Signed-off-by: joseph-sentry --- .../static_analysis/test_static_analysis_service.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/services/static_analysis/test_static_analysis_service.py b/tests/services/static_analysis/test_static_analysis_service.py index 08876634..c8721d98 100644 --- a/tests/services/static_analysis/test_static_analysis_service.py +++ b/tests/services/static_analysis/test_static_analysis_service.py @@ -32,9 +32,7 @@ async def test_process_files_with_error(self, mocker): ], ) ) - mock_get_context = mocker.patch( - "codecov_cli.services.staticanalysis.get_context" - ) + mock_pool = mocker.patch("codecov_cli.services.staticanalysis.Pool") def side_effect(config, filename: FileAnalysisRequest): if filename.result_filename == "correct_file.py": @@ -59,12 +57,12 @@ def imap_side_effect(mapped_func, files): results.append(mapped_func(file)) return results - mock_get_context.return_value.Pool.return_value.__enter__.return_value.imap_unordered.side_effect = ( + mock_pool.return_value.__enter__.return_value.imap_unordered.side_effect = ( imap_side_effect ) results = await process_files(files_found, 1, {}) - mock_get_context.return_value.Pool.return_value.__enter__.return_value.imap_unordered.assert_called() + mock_pool.return_value.__enter__.return_value.imap_unordered.assert_called() assert mock_analyze_function.call_count == 2 assert results == dict( all_data={"correct_file.py": {"hash": "abc123"}}, From 7888089c164f618b4db290eb2641f60730baebfb Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 27 Nov 2023 09:10:52 -0500 Subject: [PATCH 022/128] Add missing imports --- codecov_cli/commands/report.py | 1 + .../staticanalysis/analyzers/javascript_es6/__init__.py | 2 +- .../services/staticanalysis/analyzers/python/__init__.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/codecov_cli/commands/report.py b/codecov_cli/commands/report.py index 23474209..1e57bb29 100644 --- a/codecov_cli/commands/report.py +++ b/codecov_cli/commands/report.py @@ -2,6 +2,7 @@ import click +from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_logic diff --git a/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py b/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py index 107a34b2..e80c8254 100644 --- a/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py +++ b/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py @@ -1,8 +1,8 @@ import hashlib +import staticcodecov_languages from tree_sitter import Language, Parser -import staticcodecov_languages from codecov_cli.services.staticanalysis.analyzers.general import BaseAnalyzer from codecov_cli.services.staticanalysis.analyzers.javascript_es6.node_wrappers import ( NodeVisitor, diff --git a/codecov_cli/services/staticanalysis/analyzers/python/__init__.py b/codecov_cli/services/staticanalysis/analyzers/python/__init__.py index e535698b..1e5f5782 100644 --- a/codecov_cli/services/staticanalysis/analyzers/python/__init__.py +++ b/codecov_cli/services/staticanalysis/analyzers/python/__init__.py @@ -1,8 +1,8 @@ import hashlib +import staticcodecov_languages from tree_sitter import Language, Parser -import staticcodecov_languages from codecov_cli.services.staticanalysis.analyzers.general import BaseAnalyzer from codecov_cli.services.staticanalysis.analyzers.python.node_wrappers import ( NodeVisitor, From 6cf4f9cb2b5ccf0a095a163af9c961cc947aa228 Mon Sep 17 00:00:00 2001 From: Scott Nelson Date: Mon, 27 Nov 2023 09:21:08 -0500 Subject: [PATCH 023/128] Fix isort --- .../staticanalysis/analyzers/javascript_es6/__init__.py | 2 +- .../services/staticanalysis/analyzers/python/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py b/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py index e80c8254..107a34b2 100644 --- a/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py +++ b/codecov_cli/services/staticanalysis/analyzers/javascript_es6/__init__.py @@ -1,8 +1,8 @@ import hashlib -import staticcodecov_languages from tree_sitter import Language, Parser +import staticcodecov_languages from codecov_cli.services.staticanalysis.analyzers.general import BaseAnalyzer from codecov_cli.services.staticanalysis.analyzers.javascript_es6.node_wrappers import ( NodeVisitor, diff --git a/codecov_cli/services/staticanalysis/analyzers/python/__init__.py b/codecov_cli/services/staticanalysis/analyzers/python/__init__.py index 1e5f5782..e535698b 100644 --- a/codecov_cli/services/staticanalysis/analyzers/python/__init__.py +++ b/codecov_cli/services/staticanalysis/analyzers/python/__init__.py @@ -1,8 +1,8 @@ import hashlib -import staticcodecov_languages from tree_sitter import Language, Parser +import staticcodecov_languages from codecov_cli.services.staticanalysis.analyzers.general import BaseAnalyzer from codecov_cli.services.staticanalysis.analyzers.python.node_wrappers import ( NodeVisitor, From 96c0578bce270f06c38a2079442efd2c127fbae6 Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:55:09 -0300 Subject: [PATCH 024/128] add X-Tokenless header when uploading from fork (#335) Public forks will accept tokenless uploads. Currently we were just sending an empty header (no Authorization). These changes add a header `X-Tokenless: fork_slug` so we know easily that the request is from a fork, and which fork it's from. I also have a tendency to compulsively add typehints to complex types. --- codecov_cli/helpers/git.py | 6 ++++-- codecov_cli/helpers/git_services/__init__.py | 14 ++++++++++++++ codecov_cli/helpers/git_services/github.py | 4 +++- codecov_cli/services/commit/__init__.py | 2 +- codecov_cli/services/report/__init__.py | 7 ++++--- codecov_cli/services/upload/upload_sender.py | 10 +++++----- tests/services/commit/test_commit_service.py | 2 +- 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index 5c79ae6e..2b4fcde6 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -1,9 +1,11 @@ import logging import re from enum import Enum +from typing import Optional from urllib.parse import urlparse from codecov_cli.helpers.encoder import decode_slug +from codecov_cli.helpers.git_services import PullDict from codecov_cli.helpers.git_services.github import Github slug_regex = re.compile(r"[^/\s]+\/[^/\s]+$") @@ -92,7 +94,7 @@ def parse_git_service(remote_repo_url: str): return None -def is_fork_pr(pull_dict): +def is_fork_pr(pull_dict: PullDict) -> bool: """ takes in dict: pull_dict returns true if PR is made in a fork context, false if not. @@ -100,7 +102,7 @@ def is_fork_pr(pull_dict): return pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"] -def get_pull(service, slug, pr_num): +def get_pull(service, slug, pr_num) -> Optional[PullDict]: """ takes in str git service e.g. github, gitlab etc., slug in the owner/repo format, and the pull request number returns the pull request info gotten from the git service provider if successful, None if not diff --git a/codecov_cli/helpers/git_services/__init__.py b/codecov_cli/helpers/git_services/__init__.py index e69de29b..a1e580cf 100644 --- a/codecov_cli/helpers/git_services/__init__.py +++ b/codecov_cli/helpers/git_services/__init__.py @@ -0,0 +1,14 @@ +from typing import TypedDict + + +class CommitInfo(TypedDict): + sha: str + label: str + ref: str + slug: str + + +class PullDict(TypedDict): + url: str + head: CommitInfo + base: CommitInfo diff --git a/codecov_cli/helpers/git_services/github.py b/codecov_cli/helpers/git_services/github.py index e52efe80..1d9e8534 100644 --- a/codecov_cli/helpers/git_services/github.py +++ b/codecov_cli/helpers/git_services/github.py @@ -2,12 +2,14 @@ import requests +from codecov_cli.helpers.git_services import PullDict + class Github: api_url = "https://api.github.com" api_version = "2022-11-28" - def get_pull_request(self, slug, pr_number): + def get_pull_request(self, slug, pr_number) -> PullDict: pull_url = f"/repos/{slug}/pulls/{pr_number}" url = self.api_url + pull_url headers = {"X-GitHub-Api-Version": self.api_version} diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index 05b1bce4..69d2e602 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -46,7 +46,7 @@ def send_commit_data( decoded_slug = decode_slug(slug) pull_dict = get_pull(service, decoded_slug, pr) if not token else None if is_fork_pr(pull_dict): - headers = {} + headers = {"X-Tokenless": pull_dict["head"]["slug"]} branch = pull_dict["head"]["slug"] + ":" + branch logger.info("The PR is happening in a forked repo. Using tokenless upload.") else: diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 04a38e87..73aa2d6b 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -51,9 +51,10 @@ def send_create_report_request( pull_dict = ( get_pull(service, decoded_slug, pull_request_number) if not token else None ) - headers = ( - {} if not token and is_fork_pr(pull_dict) else get_token_header_or_fail(token) - ) + if is_fork_pr(pull_dict): + headers = {"X-Tokenless": pull_dict["head"]["slug"]} + else: + headers = get_token_header_or_fail(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" return send_post_request(url=url, headers=headers, data=data) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index 8f34eb12..d1cedb1c 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -56,11 +56,11 @@ def send_upload_data( pull_dict = ( get_pull(git_service, slug, pull_request_number) if not token else None ) - headers = ( - {} - if not token and is_fork_pr(pull_dict) - else get_token_header_or_fail(token) - ) + + if is_fork_pr(pull_dict): + headers = {"X-Tokenless": pull_dict["head"]["slug"]} + else: + headers = get_token_header_or_fail(token) encoded_slug = encode_slug(slug) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/uploads" diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 2aeff136..f71f1bea 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -195,5 +195,5 @@ def mock_request(*args, headers={}, **kwargs): "pullid": "1", "branch": "user_forked_repo/codecov-cli:branch", }, - headers={}, + headers={"X-Tokenless": "user_forked_repo/codecov-cli"}, ) From 47aa664128db4bff00de149d410d401a2b9d19ab Mon Sep 17 00:00:00 2001 From: codecov-releaser Date: Thu, 7 Dec 2023 19:21:35 +0000 Subject: [PATCH 025/128] Prepare release 0.4.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 988a916c..7102bede 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.1", + version="0.4.2", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 69ab1f276b30763fd33d4637d94f3c7f2821159c Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:58:37 -0400 Subject: [PATCH 026/128] Update release workflow to be consistent with other repos --- .github/workflows/create_release.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index b4e93b07..c0b75dd8 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -9,8 +9,6 @@ on: jobs: create-release: if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') && github.repository_owner == 'codecov' }} - env: - GITHUB_TOKEN: ${{ secrets.CODECOV_RELEASE_PAT }} name: Create Github Release runs-on: ubuntu-latest steps: @@ -22,11 +20,8 @@ jobs: run: | echo release_version=$(grep -E "version=\"[0-9]\.[0-9]\.[0-9]\"" setup.py | grep -Eo "[0-9]\.[0-9]\.[0-9]") >> "$GITHUB_OUTPUT" - - name: Create GH Release - uses: softprops/action-gh-release@v0.1.15 - with: - name: Release v${{ steps.get-release-vars.outputs.release_version }} - tag_name: v${{ steps.get-release-vars.outputs.release_version }} - generate_release_notes: true - body: Autogenerated for ${{ steps.get-release-vars.outputs.release_version }}. Created for ${{ github.event.pull_request.html_url }} - + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.CODECOV_RELEASE_PAT }} + run: | + gh release create v${{ steps.get-release-vars.outputs.release_version }} --title "Release v${{ steps.get-release-vars.outputs.release_version }}" --notes "Autogenerated for v${{ steps.get-release-vars.outputs.release_version }}. Created for ${{ github.event.pull_request.html_url }}" --generate-notes --target ${{ github.event.pull_request.head.sha }} From e7dec66c409aee830c67a826e9161533bb48ef4c Mon Sep 17 00:00:00 2001 From: trent-codecov Date: Tue, 12 Dec 2023 12:34:05 -0500 Subject: [PATCH 027/128] Update release workflow to be consistent with other repos --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7102bede..988a916c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.2", + version="0.4.1", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 44fdedf0fa55886cf4ac3a392108184f0a70c50a Mon Sep 17 00:00:00 2001 From: codecov-releaser Date: Tue, 12 Dec 2023 18:48:30 +0000 Subject: [PATCH 028/128] Prepare release 0.4.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 988a916c..7102bede 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.1", + version="0.4.2", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 0ae5f0d150ab2bb3bb236dd9e7bbfd02b45a2f0f Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:21:08 -0300 Subject: [PATCH 029/128] feat: add X-Tokenless-PR header (#342) * feat: add X-Tokenless-PR header If you look at https://github.com/codecov/codecov-api/pull/304 you'll notice that it expects 2 headers - `X-Tokenless` and `X-Tokenless-PR`. We were not yet sending `X-Tokenless-PR` (because I only realized we'd need it after doing the API side of things). These changes add that header to tokenless requests. * improve coverage * fix: exception if PR is from same repo If a PR exists and base and head are from the same repo it seems that the response["head"]["repo"] is set to None, causing exception when the code runs. This fixes that bug so that the CLI can fail gracefully (if a token is not provided) --- .gitignore | 2 + codecov_cli/helpers/git_services/github.py | 6 ++- codecov_cli/services/commit/__init__.py | 4 +- codecov_cli/services/report/__init__.py | 5 ++- codecov_cli/services/upload/upload_sender.py | 5 ++- tests/helpers/test_upload_sender.py | 41 ++++++++++++++++++++ tests/services/commit/test_commit_service.py | 2 +- tests/services/report/test_report_service.py | 37 ++++++++++++++++++ 8 files changed, 96 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index ea23c4f1..62c63e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,5 @@ cython_debug/ # Vim temporary files *.swp *.swo + +.debug \ No newline at end of file diff --git a/codecov_cli/helpers/git_services/github.py b/codecov_cli/helpers/git_services/github.py index 1d9e8534..2e7551bb 100644 --- a/codecov_cli/helpers/git_services/github.py +++ b/codecov_cli/helpers/git_services/github.py @@ -22,7 +22,11 @@ def get_pull_request(self, slug, pr_number) -> PullDict: "sha": res["head"]["sha"], "label": res["head"]["label"], "ref": res["head"]["ref"], - "slug": res["head"]["repo"]["full_name"], + # Through empiric test data it seems that the "repo" key in "head" is set to None + # If the PR is from the same repo (e.g. not from a fork) + "slug": res["head"]["repo"]["full_name"] + if res["head"]["repo"] + else res["base"]["repo"]["full_name"], }, "base": { "sha": res["base"]["sha"], diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index 69d2e602..2c8cbdd4 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -3,7 +3,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug -from codecov_cli.helpers.git import get_git_service, get_pull, is_fork_pr +from codecov_cli.helpers.git import get_pull, is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -46,7 +46,7 @@ def send_commit_data( decoded_slug = decode_slug(slug) pull_dict = get_pull(service, decoded_slug, pr) if not token else None if is_fork_pr(pull_dict): - headers = {"X-Tokenless": pull_dict["head"]["slug"]} + headers = {"X-Tokenless": pull_dict["head"]["slug"], "X-Tokenless-PR": pr} branch = pull_dict["head"]["slug"] + ":" + branch logger.info("The PR is happening in a forked repo. Using tokenless upload.") else: diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 73aa2d6b..1b3cf16a 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -52,7 +52,10 @@ def send_create_report_request( get_pull(service, decoded_slug, pull_request_number) if not token else None ) if is_fork_pr(pull_dict): - headers = {"X-Tokenless": pull_dict["head"]["slug"]} + headers = { + "X-Tokenless": pull_dict["head"]["slug"], + "X-Tokenless-PR": pull_request_number, + } else: headers = get_token_header_or_fail(token) upload_url = enterprise_url or CODECOV_API_URL diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index d1cedb1c..f500f71b 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -58,7 +58,10 @@ def send_upload_data( ) if is_fork_pr(pull_dict): - headers = {"X-Tokenless": pull_dict["head"]["slug"]} + headers = { + "X-Tokenless": pull_dict["head"]["slug"], + "X-Tokenless-PR": pull_request_number, + } else: headers = get_token_header_or_fail(token) encoded_slug = encode_slug(slug) diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 2cc79f85..ec54d3b3 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -159,6 +159,47 @@ def test_upload_sender_post_called_with_right_parameters( post_req_made.headers.items() >= headers.items() ) # test dict is a subset of the other + def test_upload_sender_post_called_with_right_parameters_tokenless( + self, + mocked_responses, + mocked_legacy_upload_endpoint, + mocked_storage_server, + mocker, + ): + headers = {"X-Tokenless": "user-forked/repo", "X-Tokenless-PR": "pr"} + mock_get_pull = mocker.patch( + "codecov_cli.services.upload.upload_sender.get_pull", + return_value={ + "head": {"slug": "user-forked/repo"}, + "base": {"slug": "org/repo"}, + }, + ) + mocked_legacy_upload_endpoint.match = [ + matchers.json_params_matcher(request_data), + matchers.header_matcher(headers), + ] + + sending_result = UploadSender().send_upload_data( + upload_collection, random_sha, None, **named_upload_data + ) + assert sending_result.error is None + assert sending_result.warnings == [] + + assert len(mocked_responses.calls) == 2 + + post_req_made = mocked_responses.calls[0].request + encoded_slug = encode_slug(named_upload_data["slug"]) + response = json.loads(mocked_responses.calls[0].response.text) + assert response.get("url") == "https://app.codecov.io/commit-url" + assert ( + post_req_made.url + == f"https://api.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads" + ) + assert ( + post_req_made.headers.items() >= headers.items() + ) # test dict is a subset of the other + mock_get_pull.assert_called() + def test_upload_sender_put_called_with_right_parameters( self, mocked_responses, mocked_legacy_upload_endpoint, mocked_storage_server ): diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index f71f1bea..2b64e8ee 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -195,5 +195,5 @@ def mock_request(*args, headers={}, **kwargs): "pullid": "1", "branch": "user_forked_repo/codecov-cli:branch", }, - headers={"X-Tokenless": "user_forked_repo/codecov-cli"}, + headers={"X-Tokenless": "user_forked_repo/codecov-cli", "X-Tokenless-PR": "1"}, ) diff --git a/tests/services/report/test_report_service.py b/tests/services/report/test_report_service.py index 8d31a112..e9987abe 100644 --- a/tests/services/report/test_report_service.py +++ b/tests/services/report/test_report_service.py @@ -26,6 +26,43 @@ def test_send_create_report_request_200(mocker): mocked_response.assert_called_once() +def test_send_create_report_request_200_tokneless(mocker): + mocked_response = mocker.patch( + "codecov_cli.services.report.send_post_request", + return_value=RequestResult( + status_code=200, + error=None, + warnings=[], + text="mocked response", + ), + ) + + mocked_get_pull = mocker.patch( + "codecov_cli.services.report.get_pull", + return_value={ + "head": {"slug": "user-forked/repo"}, + "base": {"slug": "org/repo"}, + }, + ) + res = send_create_report_request( + "commit_sha", + "code", + "github", + None, + "owner::::repo", + "enterprise_url", + 1, + ) + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_with( + url=f"enterprise_url/upload/github/owner::::repo/commits/commit_sha/reports", + headers={"X-Tokenless": "user-forked/repo", "X-Tokenless-PR": 1}, + data={"code": "code"}, + ) + mocked_get_pull.assert_called() + + def test_send_create_report_request_403(mocker): mocked_response = mocker.patch( "codecov_cli.services.report.requests.post", From 19186494bde1e8e850419c49bbd25a3a495c2a92 Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:15:03 -0300 Subject: [PATCH 030/128] chore: rename 'pytest_command' to 'python_path' (#344) We add 'pytest_command' so that users can specify the pytest command to use. That is still confusing for users to setup the config. Plus people are more used to give a python path to execute. So I think it's good idea to replace 'pytest_command' to the simpler 'python_path'. --- codecov_cli/runners/pytest_standard_runner.py | 14 ++++++-------- tests/runners/test_pytest_standard_runner.py | 12 ++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/codecov_cli/runners/pytest_standard_runner.py b/codecov_cli/runners/pytest_standard_runner.py index 5a4f692c..9d6e72dc 100644 --- a/codecov_cli/runners/pytest_standard_runner.py +++ b/codecov_cli/runners/pytest_standard_runner.py @@ -17,12 +17,9 @@ class PytestStandardRunnerConfigParams(dict): @property - def pytest_command(self) -> List[str]: - command_from_config = self.get("pytest_command") - if isinstance(command_from_config, str): - logger.warning("pytest_command should be a list") - command_from_config = command_from_config.split(" ") - return command_from_config or ["python", "-m", "pytest"] + def python_path(self) -> str: + python_path = self.get("python_path") + return python_path or "python" @property def collect_tests_options(self) -> List[str]: @@ -49,6 +46,7 @@ def coverage_root(self) -> str: class PytestStandardRunner(LabelAnalysisRunnerInterface): dry_run_runner_options = ["--cov-context=test"] + params: PytestStandardRunnerConfigParams def __init__(self, config_params: Optional[dict] = None) -> None: super().__init__() @@ -70,7 +68,7 @@ def _execute_pytest(self, pytest_args: List[str], capture_output: bool = True): Raises Exception if pytest fails Returns the complete pytest output """ - command = self.params.pytest_command + pytest_args + command = [self.params.python_path, "-m", "pytest"] + pytest_args try: result = subprocess.run( command, @@ -101,7 +99,7 @@ def collect_tests(self): "Collecting tests", extra=dict( extra_log_attributes=dict( - pytest_command=self.params.pytest_command, + pytest_command=[self.params.python_path, "-m", "pytest"], pytest_options=options_to_use, ), ), diff --git a/tests/runners/test_pytest_standard_runner.py b/tests/runners/test_pytest_standard_runner.py index ae9866a3..20a127f1 100644 --- a/tests/runners/test_pytest_standard_runner.py +++ b/tests/runners/test_pytest_standard_runner.py @@ -39,22 +39,18 @@ def test_execute_pytest(self, mock_subprocess): ) assert result == output - @pytest.mark.parametrize( - "command_configured", [["pyenv", "pytest"], "pyenv pytest"] - ) + @pytest.mark.parametrize("python_path", ["/usr/bin/python", "venv/bin/python"]) @patch("codecov_cli.runners.pytest_standard_runner.subprocess") - def test_execute_pytest_user_provided_command( - self, mock_subprocess, command_configured - ): + def test_execute_pytest_user_provided_command(self, mock_subprocess, python_path): output = "Output in stdout" return_value = MagicMock(stdout=output.encode("utf-8")) mock_subprocess.run.return_value = return_value - runner = PytestStandardRunner(dict(pytest_command=command_configured)) + runner = PytestStandardRunner(dict(python_path=python_path)) result = runner._execute_pytest(["--option", "--ignore=batata"]) mock_subprocess.run.assert_called_with( - ["pyenv", "pytest", "--option", "--ignore=batata"], + [python_path, "-m", "pytest", "--option", "--ignore=batata"], capture_output=True, check=True, stdout=None, From 4274ae5da228bfddf6348590d67a0e6b2e6a9a19 Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Wed, 27 Dec 2023 17:15:44 -0300 Subject: [PATCH 031/128] feat: dynamic runner parameters (#345) context: codecov/engineering-team#407 Consider this a 1st iteration of this feature. A compromise between "this is easy to implement and helpful to users" and "users don't have to think to use this". The benefit is that now you can add dynamic params to your runners, the downside is that you need to know the runner configuration AND format these params accordingly. Otherwise it would be very hard to figure out exactly what parameter group a given parameter is. So, compromise. More details on whoe to actually use the feature are documented in the code. --- codecov_cli/commands/labelanalysis.py | 43 ++++++++++++++++++- codecov_cli/runners/__init__.py | 25 ++++++++--- codecov_cli/runners/pytest_standard_runner.py | 25 +++++++++++ tests/commands/test_invoke_labelanalysis.py | 15 +++++++ tests/runners/test_pytest_standard_runner.py | 31 ++++++++++++- tests/runners/test_runners.py | 35 ++++++++++++--- 6 files changed, 157 insertions(+), 17 deletions(-) diff --git a/codecov_cli/commands/labelanalysis.py b/codecov_cli/commands/labelanalysis.py index 5298854b..a2eda197 100644 --- a/codecov_cli/commands/labelanalysis.py +++ b/codecov_cli/commands/labelanalysis.py @@ -2,7 +2,7 @@ import logging import pathlib import time -from typing import List, Optional +from typing import Dict, List, Optional import click import requests @@ -70,6 +70,11 @@ help="Format in which --dry-run data is printed. Default is JSON.", default="json", ) +@click.option( + "--runner-param", + "runner_params", + multiple=True, +) @click.pass_context def label_analysis( ctx: click.Context, @@ -80,6 +85,7 @@ def label_analysis( max_wait_time: str, dry_run: bool, dry_run_format: str, + runner_params: List[str], ): enterprise_url = ctx.obj.get("enterprise_url") logger.debug( @@ -113,7 +119,8 @@ def label_analysis( codecov_yaml = ctx.obj["codecov_yaml"] or {} cli_config = codecov_yaml.get("cli", {}) # Raises error if no runner is found - runner = get_runner(cli_config, runner_name) + parsed_runner_params = _parse_runner_params(runner_params) + runner = get_runner(cli_config, runner_name, parsed_runner_params) logger.debug( f"Selected runner: {runner}", extra=dict(extra_log_attributes=dict(config=runner.params)), @@ -232,6 +239,38 @@ def label_analysis( time.sleep(5) +def _parse_runner_params(runner_params: List[str]) -> Dict[str, str]: + """Parses the structured list of dynamic runner params into a dictionary. + Structure is `key=value`. If value is a list make it comma-separated. + If the list item doesn't have '=' we consider it the key and set to None. + + EXAMPLE: + runner_params = ['key=value', 'null_item', 'list=item1,item2,item3'] + _parse_runner_params(runner_params) == { + 'key': 'value', + 'null_item': None, + 'list': ['item1', 'item2', 'item3'] + } + """ + final_params = {} + for param in runner_params: + # Emit warning if param is not well formatted + # Using == 0 rather than != 1 because there might be + # a good reason for the param to include '=' in the value. + if param.count("=") == 0: + logger.warning( + f"Runner param {param} is not well formated. Setting value to None. Use '--runner-param key=value' to set value" + ) + final_params[param] = None + else: + key, value = param.split("=", 1) + # For list values we need to split the list too + if "," in value: + value = value.split(",") + final_params[key] = value + return final_params + + def _potentially_calculate_absent_labels( request_result, requested_labels ) -> LabelAnalysisRequestResult: diff --git a/codecov_cli/runners/__init__.py b/codecov_cli/runners/__init__.py index aab02935..2c452c8e 100644 --- a/codecov_cli/runners/__init__.py +++ b/codecov_cli/runners/__init__.py @@ -15,7 +15,9 @@ class UnableToFindRunner(Exception): pass -def _load_runner_from_yaml(plugin_dict: typing.Dict) -> LabelAnalysisRunnerInterface: +def _load_runner_from_yaml( + plugin_dict: typing.Dict, dynamic_params: typing.Dict +) -> LabelAnalysisRunnerInterface: try: module_obj = import_module(plugin_dict["module"]) class_obj = getattr(module_obj, plugin_dict["class"]) @@ -32,16 +34,21 @@ def _load_runner_from_yaml(plugin_dict: typing.Dict) -> LabelAnalysisRunnerInter ) raise try: - return class_obj(**plugin_dict["params"]) + final_params = {**plugin_dict["params"], **dynamic_params} + return class_obj(**final_params) except TypeError: click.secho( - f"Unable to instantiate {class_obj} with parameters {plugin_dict['params']}", + f"Unable to instantiate {class_obj} with parameters {final_params}", err=True, ) raise -def get_runner(cli_config, runner_name) -> LabelAnalysisRunnerInterface: +def get_runner( + cli_config, runner_name: str, dynamic_params: typing.Dict = None +) -> LabelAnalysisRunnerInterface: + if dynamic_params is None: + dynamic_params = {} if runner_name == "pytest": config_params = cli_config.get("runners", {}).get("pytest", {}) # This is for backwards compatibility with versions <= 0.3.4 @@ -52,10 +59,12 @@ def get_runner(cli_config, runner_name) -> LabelAnalysisRunnerInterface: logger.warning( "Using 'python' to configure the PytestStandardRunner is deprecated. Please change to 'pytest'" ) - return PytestStandardRunner(config_params) + final_params = {**config_params, **dynamic_params} + return PytestStandardRunner(final_params) elif runner_name == "dan": config_params = cli_config.get("runners", {}).get("dan", {}) - return DoAnythingNowRunner(config_params) + final_params = {**config_params, **dynamic_params} + return DoAnythingNowRunner(final_params) logger.debug( f"Trying to load runner {runner_name}", extra=dict( @@ -65,5 +74,7 @@ def get_runner(cli_config, runner_name) -> LabelAnalysisRunnerInterface: ), ) if cli_config and runner_name in cli_config.get("runners", {}): - return _load_runner_from_yaml(cli_config["runners"][runner_name]) + return _load_runner_from_yaml( + cli_config["runners"][runner_name], dynamic_params=dynamic_params + ) raise UnableToFindRunner(f"Can't find runner {runner_name}") diff --git a/codecov_cli/runners/pytest_standard_runner.py b/codecov_cli/runners/pytest_standard_runner.py index 9d6e72dc..0ab327cf 100644 --- a/codecov_cli/runners/pytest_standard_runner.py +++ b/codecov_cli/runners/pytest_standard_runner.py @@ -1,3 +1,4 @@ +import inspect import logging import random import subprocess @@ -42,6 +43,18 @@ def coverage_root(self) -> str: """ return self.get("coverage_root", "./") + @classmethod + def get_available_params(cls) -> List[str]: + """Lists all the @property attribute names of this class. + These attributes are considered the 'valid config options' + """ + klass_methods = [ + x + for x in dir(cls) + if (inspect.isdatadescriptor(getattr(cls, x)) and not x.startswith("__")) + ] + return klass_methods + class PytestStandardRunner(LabelAnalysisRunnerInterface): @@ -52,8 +65,20 @@ def __init__(self, config_params: Optional[dict] = None) -> None: super().__init__() if config_params is None: config_params = {} + # Before we create the config params we emit warnings if any param is unknown + # So the user knows something is wrong with their config + self._possibly_warn_bad_config(config_params) self.params = PytestStandardRunnerConfigParams(config_params) + def _possibly_warn_bad_config(self, config_params: dict): + available_config_params = ( + PytestStandardRunnerConfigParams.get_available_params() + ) + provided_config_params = config_params.keys() + for provided_param in provided_config_params: + if provided_param not in available_config_params: + logger.warning(f"Config parameter '{provided_param}' is unknonw.") + def parse_captured_output_error(self, exp: CalledProcessError) -> str: result = "" for out_stream in [exp.stdout, exp.stderr]: diff --git a/tests/commands/test_invoke_labelanalysis.py b/tests/commands/test_invoke_labelanalysis.py index 8cbe3fe9..230e5b12 100644 --- a/tests/commands/test_invoke_labelanalysis.py +++ b/tests/commands/test_invoke_labelanalysis.py @@ -13,6 +13,7 @@ _dry_run_json_output, _dry_run_list_output, _fallback_to_collected_labels, + _parse_runner_params, _potentially_calculate_absent_labels, _send_labelanalysis_request, ) @@ -169,6 +170,20 @@ def test__dry_run_space_separated_list_output(self): == "TESTS_TO_RUN='--option=1' '--option=2' 'label_1' 'label_2'\nTESTS_TO_SKIP='--option=1' '--option=2' 'label_3' 'label_4'\n" ) + def test_parse_dynamic_runner_options(self): + params = [ + "wrong_param", + "key=value", + "list_key=val1,val2,val3", + "point=somethingwith=sign", + ] + assert _parse_runner_params(params) == { + "wrong_param": None, + "key": "value", + "list_key": ["val1", "val2", "val3"], + "point": "somethingwith=sign", + } + class TestLabelAnalysisCommand(object): def test_invoke_label_analysis_missing_token(self, mocker, fake_ci_provider): diff --git a/tests/runners/test_pytest_standard_runner.py b/tests/runners/test_pytest_standard_runner.py index 20a127f1..aadbcd3f 100644 --- a/tests/runners/test_pytest_standard_runner.py +++ b/tests/runners/test_pytest_standard_runner.py @@ -5,7 +5,10 @@ import pytest from pytest import ExitCode -from codecov_cli.runners.pytest_standard_runner import PytestStandardRunner +from codecov_cli.runners.pytest_standard_runner import ( + PytestStandardRunner, + PytestStandardRunnerConfigParams, +) from codecov_cli.runners.pytest_standard_runner import logger as runner_logger from codecov_cli.runners.pytest_standard_runner import stdout as pyrunner_stdout from codecov_cli.runners.types import LabelAnalysisRequestResult @@ -39,9 +42,33 @@ def test_execute_pytest(self, mock_subprocess): ) assert result == output + @patch("codecov_cli.runners.pytest_standard_runner.logger.warning") + def test_warning_bad_config(self, mock_warning): + available_config = PytestStandardRunnerConfigParams.get_available_params() + assert "python_path" in available_config + assert "collect_tests_options" in available_config + assert "some_missing_option" not in available_config + params = dict( + python_path="path_to_python", + collect_tests_options=["option1", "option2"], + some_missing_option="option", + ) + runner = PytestStandardRunner(params) + # Adding invalid config options emits a warning + assert mock_warning.called_with( + "Config parameter 'some_missing_option' is unknonw." + ) + # Warnings don't change the config + assert runner.params == {**params, "some_missing_option": "option"} + # And we can still access the config as usual + assert runner.params.python_path == "path_to_python" + assert runner.params.collect_tests_options == ["option1", "option2"] + @pytest.mark.parametrize("python_path", ["/usr/bin/python", "venv/bin/python"]) @patch("codecov_cli.runners.pytest_standard_runner.subprocess") - def test_execute_pytest_user_provided_command(self, mock_subprocess, python_path): + def test_execute_pytest_user_provided_python_path( + self, mock_subprocess, python_path + ): output = "Output in stdout" return_value = MagicMock(stdout=output.encode("utf-8")) mock_subprocess.run.return_value = return_value diff --git a/tests/runners/test_runners.py b/tests/runners/test_runners.py index 5e869bf6..8a813d7e 100644 --- a/tests/runners/test_runners.py +++ b/tests/runners/test_runners.py @@ -14,7 +14,7 @@ def test_get_standard_runners(self): assert isinstance(get_runner({}, "dan"), DoAnythingNowRunner) # TODO: Extend with other standard runners once we create them (e.g. JS) - def test_pytest_standard_runner_with_options_backwards_compatible(self): + def test_pytest_standard_runner_with_options(self): config_params = dict( collect_tests_options=["--option=value", "-option"], ) @@ -26,6 +26,23 @@ def test_pytest_standard_runner_with_options_backwards_compatible(self): ) assert runner_instance.params.coverage_root == "./" + def test_pytest_standard_runner_with_options_and_dynamic_options(self): + config_params = dict( + collect_tests_options=["--option=value", "-option"], + ) + runner_instance = get_runner( + {"runners": {"pytest": config_params}}, + "pytest", + {"python_path": "path/to/python"}, + ) + assert isinstance(runner_instance, PytestStandardRunner) + assert ( + runner_instance.params.collect_tests_options + == config_params["collect_tests_options"] + ) + assert runner_instance.params.python_path == "path/to/python" + assert runner_instance.params.coverage_root == "./" + def test_pytest_standard_runner_with_options_backwards_compatible(self): config_params = dict( collect_tests_options=["--option=value", "-option"], @@ -56,7 +73,9 @@ def test_get_runner_from_yaml(self, mock_load_runner): config = {"runners": {"my_runner": {"path": "path_to_my_runner"}}} mock_load_runner.return_value = "MyRunner()" assert get_runner(config, "my_runner") == "MyRunner()" - mock_load_runner.assert_called_with({"path": "path_to_my_runner"}) + mock_load_runner.assert_called_with( + {"path": "path_to_my_runner"}, dynamic_params={} + ) def test_load_runner_from_yaml(self, mocker): fake_module = mocker.MagicMock(FakeRunner=FakeRunner) @@ -66,7 +85,8 @@ def test_load_runner_from_yaml(self, mocker): "module": "mymodule.runner", "class": "FakeRunner", "params": {"collect_tests_response": ["list", "of", "labels"]}, - } + }, + {}, ) assert isinstance(res, FakeRunner) assert res.collect_tests() == ["list", "of", "labels"] @@ -83,7 +103,8 @@ def side_effect(*args, **kwargs): "module": "mymodule.runner", "class": "FakeRunner", "params": {"collect_tests_response": ["list", "of", "labels"]}, - } + }, + {}, ) def test_load_runner_from_yaml_class_not_found(self, mocker): @@ -97,7 +118,8 @@ def test_load_runner_from_yaml_class_not_found(self, mocker): "module": "mymodule.runner", "class": "WrongClassName", "params": {"collect_tests_response": ["list", "of", "labels"]}, - } + }, + {}, ) def test_load_runner_from_yaml_fail_instantiate_class(self, mocker): @@ -109,5 +131,6 @@ def test_load_runner_from_yaml_fail_instantiate_class(self, mocker): "module": "mymodule.runner", "class": "FakeRunner", "params": {"wrong_params": ["list", "of", "labels"]}, - } + }, + {}, ) From ab1c3261cb3821956924fe088e7381d7dada85fc Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 3 Jan 2024 11:27:31 -0500 Subject: [PATCH 032/128] Prepare release 0.4.3 (#348) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7102bede..f4a731d7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.2", + version="0.4.3", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 145701f0a75251852dd681842b57e8b03e61675e Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Fri, 5 Jan 2024 14:50:54 -0300 Subject: [PATCH 033/128] docs: add empty-upload cmd section (#349) --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 050e846c..f55cef33 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ CodecovCLI is a new way for users to interact with Codecov directly from the use - [create-report-results](#create-report-results) - [get-report-results](#get-report-results) - [pr-base-picking](#pr-base-picking) + - [send-notifications](#send-notifications) + - [empty-upload](#empty-upload) - [How to Use Local Upload](#how-to-use-local-upload) - [Work in Progress Features](#work-in-progress-features) - [Plugin System](#plugin-system) @@ -27,6 +29,7 @@ CodecovCLI is a new way for users to interact with Codecov directly from the use - [Contributions](#contributions) - [Requirements](#requirements) - [Guidelines](#guidelines) + - [Dependencies](#dependencies) - [Releases](#releases) # Installing @@ -229,6 +232,23 @@ Codecov-cli supports user input. These inputs, along with their descriptions and | --git-service | Git provider. Options: github, gitlab, bitbucket, github_enterprise, gitlab_enterprise, bitbucket_server | Optional | -h,--help |Show this message and exit. +## empty-upload + +Used if the changes made don't need testing, but PRs require a passing codecov status to be merged. +This command will scan the files in the commit and send passing status checks configured if all the changed files +are ignored by codecov (including README and configuration files) + +`Usage: codecovcli empty-upload [OPTIONS]` + +Options: + -C, --sha, --commit-sha TEXT Commit SHA (with 40 chars) [required] + -Z, --fail-on-error Exit with non-zero code in case of error + --git-service [github|gitlab|bitbucket|github_enterprise|gitlab_enterprise|bitbucket_server] + -t, --token TEXT Codecov upload token + -r, --slug TEXT owner/repo slug used instead of the private + repo token in Self-hosted + -h, --help Show this message and exit. + # How to Use Local Upload The CLI also supports "dry run" local uploading. This is useful if you prefer to see Codecov status checks and coverage reporting locally, in your terminal, as opposed to opening a PR and waiting for your full CI to run. Local uploads do not interfere with regular uploads made from your CI for any given commit / Pull Request. From cc954944a29de05ce66f648108fd826b3ed693c9 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 7 Dec 2023 11:52:19 -0500 Subject: [PATCH 034/128] build: add CI job for building Alpine and ARM64 linux images --- .github/workflows/build_assets.yml | 51 ++++++++++++++++++++++++++++-- scripts/build_alpine_arm.sh | 9 ++++++ scripts/build_linux_arm.sh | 8 +++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 scripts/build_alpine_arm.sh create mode 100644 scripts/build_linux_arm.sh diff --git a/.github/workflows/build_assets.yml b/.github/workflows/build_assets.yml index 8c02cc61..e541c5f3 100644 --- a/.github/workflows/build_assets.yml +++ b/.github/workflows/build_assets.yml @@ -17,8 +17,6 @@ jobs: matrix: include: - os: macos-latest - env: - CFLAGS: -arch arm64 -arch x86_64 TARGET: macos CMD_REQS: > mkdir -p pip-packages && cd pip-packages && pip wheel --no-cache-dir --no-binary tree_sitter,ijson,charset_normalizer,PyYAML .. && cd .. && @@ -83,5 +81,54 @@ jobs: tag: ${{ github.ref }} overwrite: true + build_assets_alpine_arm: + name: Build assets - Alpine and ARM + runs-on: ubuntu-latest + strategy: + matrix: + include: + - distro: "python:3.11-alpine3.18" + arch: arm64 + distro_name: alpine + - distro: "python:3.11-alpine3.18" + arch: x86_64 + distro_name: alpine + - distro: "python:3.11" + arch: arm64 + distro_name: linux + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: ${{ matrix.arch }} + - name: Run in Docker + run: | + docker run \ + --rm \ + -v $(pwd):/${{ github.workspace }} \ + -w ${{ github.workspace }} \ + --platform linux/${{ matrix.arch }} \ + ${{ matrix.distro }} \ + ./scripts/build_${{ matrix.distro_name }}_arm.sh ${{ matrix.distro_name }}_${{ matrix.arch }} + - name: Upload a Build Artifact + uses: actions/upload-artifact@v3.1.3 + if: inputs.release == false + with: + path: ./dist/codecovcli_${{ matrix.distro_name }}_${{ matrix.arch }} + - name: Upload Release Asset + if: inputs.release == true + id: upload-release-asset + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./dist/codecovcli_${{ matrix.distro_name }}_${{ matrix.arch }} + asset_name: codecovcli_${{ matrix.distro_name }}_${{ matrix.arch }} + tag: ${{ github.ref }} + overwrite: true + diff --git a/scripts/build_alpine_arm.sh b/scripts/build_alpine_arm.sh new file mode 100644 index 00000000..1c220bac --- /dev/null +++ b/scripts/build_alpine_arm.sh @@ -0,0 +1,9 @@ +#!/bin/sh +apk add musl-dev build-base +pip install -r requirements.txt +pip install . +python setup.py build +STATICCODECOV_LIB_PATH=$(find build/ -maxdepth 1 -type d -name 'lib.*' -print -quit | xargs -I {} sh -c "find {} -type f -name 'staticcodecov*' -print -quit | sed 's|^./||'") +pip install pyinstaller +pyinstaller --add-binary ${STATICCODECOV_LIB_PATH}:. --copy-metadata codecov-cli --hidden-import staticcodecov_languages -F codecov_cli/main.py +cp ./dist/main ./dist/codecovcli_$1 \ No newline at end of file diff --git a/scripts/build_linux_arm.sh b/scripts/build_linux_arm.sh new file mode 100644 index 00000000..2356bdce --- /dev/null +++ b/scripts/build_linux_arm.sh @@ -0,0 +1,8 @@ +#!/bin/sh +apt install build-essential +pip install -r requirements.txt +pip install . +python setup.py build +STATICCODECOV_LIB_PATH=$(find build/ -maxdepth 1 -type d -name 'lib.*' -print -quit | xargs -I {} sh -c "find {} -type f -name 'staticcodecov*' -print -quit | sed 's|^./||'") +pyinstaller --add-binary ${STATICCODECOV_LIB_PATH}:. --copy-metadata codecov-cli --hidden-import staticcodecov_languages -F codecov_cli/main.py +cp ./dist/main ./dist/codecovcli_$1 \ No newline at end of file From fa3ba26d76738d1ea977cace9d2f9f0f938a3cde Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 7 Dec 2023 12:04:01 -0500 Subject: [PATCH 035/128] chore: change perms for scripts Signed-off-by: joseph-sentry --- scripts/build_alpine_arm.sh | 0 scripts/build_linux_arm.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/build_alpine_arm.sh mode change 100644 => 100755 scripts/build_linux_arm.sh diff --git a/scripts/build_alpine_arm.sh b/scripts/build_alpine_arm.sh old mode 100644 new mode 100755 diff --git a/scripts/build_linux_arm.sh b/scripts/build_linux_arm.sh old mode 100644 new mode 100755 From 57fffe7ded83cdb5b927d97cc5fb89e9bcf7e170 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:58:18 -0500 Subject: [PATCH 036/128] fix: update build_linux_arm to install pyinstaller --- scripts/build_linux_arm.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_linux_arm.sh b/scripts/build_linux_arm.sh index 2356bdce..f90abd02 100755 --- a/scripts/build_linux_arm.sh +++ b/scripts/build_linux_arm.sh @@ -4,5 +4,6 @@ pip install -r requirements.txt pip install . python setup.py build STATICCODECOV_LIB_PATH=$(find build/ -maxdepth 1 -type d -name 'lib.*' -print -quit | xargs -I {} sh -c "find {} -type f -name 'staticcodecov*' -print -quit | sed 's|^./||'") +pip install pyinstaller pyinstaller --add-binary ${STATICCODECOV_LIB_PATH}:. --copy-metadata codecov-cli --hidden-import staticcodecov_languages -F codecov_cli/main.py cp ./dist/main ./dist/codecovcli_$1 \ No newline at end of file From cf80b07bbe5e5842b66923088d2ee82e720908fa Mon Sep 17 00:00:00 2001 From: codecov-releaser Date: Thu, 11 Jan 2024 15:30:46 +0000 Subject: [PATCH 037/128] Prepare release 0.4.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f4a731d7..977fce71 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.3", + version="0.4.4", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 75855aef0a551c815c2ce60b13dfdee2424b8075 Mon Sep 17 00:00:00 2001 From: Tom Hu Date: Wed, 17 Jan 2024 13:21:53 -0800 Subject: [PATCH 038/128] chore(ci): add fossa workflow --- .github/workflows/enforce-license-compliance.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/enforce-license-compliance.yml diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml new file mode 100644 index 00000000..86be7410 --- /dev/null +++ b/.github/workflows/enforce-license-compliance.yml @@ -0,0 +1,14 @@ +name: Enforce License Compliance + +on: + pull_request: + branches: [main, master] + +jobs: + enforce-license-compliance: + runs-on: ubuntu-latest + steps: + - name: 'Enforce License Compliance' + uses: getsentry/action-enforce-license-compliance@57ba820387a1a9315a46115ee276b2968da51f3d # main + with: + fossa_api_key: ${{ secrets.FOSSA_API_KEY }} From ad1249cdf745c2c048df74a97e75f7589d422438 Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:33:14 -0300 Subject: [PATCH 039/128] fix: correctly generate network if path has spaces (#357) Parsing of output of the `ls-files` commands was splitting paths with spaces. It is literally the difference show below. ```python >>> s = 'string space\nother line\n' >>> s.split() ['string', 'space', 'other', 'line'] >>> s.split('\n') ['string space', 'other line', ''] ``` closes: codecov/codecov-cli#356 --- codecov_cli/helpers/versioning_systems.py | 2 +- tests/helpers/test_folder_searcher.py | 2 ++ tests/helpers/test_versioning_systems.py | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/codecov_cli/helpers/versioning_systems.py b/codecov_cli/helpers/versioning_systems.py index 9de78419..ae4bdc72 100644 --- a/codecov_cli/helpers/versioning_systems.py +++ b/codecov_cli/helpers/versioning_systems.py @@ -118,7 +118,7 @@ def list_relevant_files( filename[1:-1] if filename.startswith('"') and filename.endswith('"') else filename - for filename in res.stdout.decode("unicode_escape").strip().split() + for filename in res.stdout.decode("unicode_escape").strip().split("\n") ] diff --git a/tests/helpers/test_folder_searcher.py b/tests/helpers/test_folder_searcher.py index 3523dbee..a6ab9895 100644 --- a/tests/helpers/test_folder_searcher.py +++ b/tests/helpers/test_folder_searcher.py @@ -43,6 +43,7 @@ def test_search_files_with_folder_exclusion(tmp_path): "another/some/banana.py", "from/some/banana.py", "to/some/banana.py", + "path/folder with space/banana.py", "apple.py", "banana.py", ] @@ -56,6 +57,7 @@ def test_search_files_with_folder_exclusion(tmp_path): tmp_path / "banana.py", tmp_path / "from/some/banana.py", tmp_path / "another/some/banana.py", + tmp_path / "path/folder with space/banana.py", ] ) assert expected_results == sorted( diff --git a/tests/helpers/test_versioning_systems.py b/tests/helpers/test_versioning_systems.py index 70e0ad20..c0532ef6 100644 --- a/tests/helpers/test_versioning_systems.py +++ b/tests/helpers/test_versioning_systems.py @@ -95,7 +95,7 @@ def test_list_relevant_files_returns_correct_network_files(self, mocker, tmp_pat return_value=mocked_subprocess, ) # git ls-files diplays a single \n as \\\\n - mocked_subprocess.stdout = b'a.txt\nb.txt\n"a\\\\nb.txt"\nc.txt\nd.txt' + mocked_subprocess.stdout = b'a.txt\nb.txt\n"a\\\\nb.txt"\nc.txt\nd.txt\n.circleci/config.yml\nLICENSE\napp/advanced calculations/advanced_calculator.js\n' vs = GitVersioningSystem() @@ -105,6 +105,9 @@ def test_list_relevant_files_returns_correct_network_files(self, mocker, tmp_pat "a\\nb.txt", "c.txt", "d.txt", + ".circleci/config.yml", + "LICENSE", + "app/advanced calculations/advanced_calculator.js", ] def test_list_relevant_files_fails_if_no_root_is_found(self, mocker): From f3827b3ed915f689c425b8d3986b28835973318f Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 25 Jan 2024 12:25:03 -0500 Subject: [PATCH 040/128] Prepare release 0.4.5 (#358) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 977fce71..81b55ce7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.4", + version="0.4.5", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From be85cc87c732d608063c281059502af0c58db2a7 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:56:23 -0500 Subject: [PATCH 041/128] Add support for test result ingestion in the CLI (#347) * feat: add test result file finding in FileFinder * feat: update upload collector to handle test result files * feat: update upload senders to handle test result files * feat: add report type option and rename coverage files options to files * tests: add tests for test_results * fix: fix file finder excluded patterns * tests: update file finder tests * tests: add tests for generate_upload_data * fix: move url and data choice to separate function in upload sender * fix: no more prep plugins in test results upload * fix: pass report type to file finder constructor * test: fix tests due to prep plugin and file finder changes Signed-off-by: joseph-sentry --- codecov_cli/commands/upload.py | 40 +++-- codecov_cli/commands/upload_process.py | 39 ++-- codecov_cli/services/upload/__init__.py | 28 +-- ...coverage_file_finder.py => file_finder.py} | 60 ++++--- .../services/upload/legacy_upload_sender.py | 6 +- .../services/upload/upload_collector.py | 24 +-- codecov_cli/services/upload/upload_sender.py | 69 +++++-- codecov_cli/types.py | 4 +- tests/commands/test_invoke_upload_process.py | 10 +- tests/helpers/test_legacy_upload_sender.py | 7 +- tests/helpers/test_upload_sender.py | 67 ++++++- .../upload/test_coverage_file_finder.py | 95 +++++++--- .../services/upload/test_upload_collector.py | 65 +++++++ tests/services/upload/test_upload_service.py | 169 ++++++++++++++---- 14 files changed, 513 insertions(+), 170 deletions(-) rename codecov_cli/services/upload/{coverage_file_finder.py => file_finder.py} (77%) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index 9ee04b99..fafb34af 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -33,7 +33,8 @@ def _turn_env_vars_into_dict(ctx, params, value): "-s", "--dir", "--coverage-files-search-root-folder", - "coverage_files_search_root_folder", + "--files-search-root-folder", + "files_search_root_folder", help="Folder where to search for coverage files", type=click.Path(path_type=pathlib.Path), default=pathlib.Path.cwd, @@ -42,7 +43,8 @@ def _turn_env_vars_into_dict(ctx, params, value): click.option( "--exclude", "--coverage-files-search-exclude-folder", - "coverage_files_search_exclude_folders", + "--files-search-exclude-folder", + "files_search_exclude_folders", help="Folders to exclude from search", type=click.Path(path_type=pathlib.Path), multiple=True, @@ -52,7 +54,8 @@ def _turn_env_vars_into_dict(ctx, params, value): "-f", "--file", "--coverage-files-search-direct-file", - "coverage_files_search_explicitly_listed_files", + "--files-search-direct-file", + "files_search_explicitly_listed_files", help="Explicit files to upload. These will be added to the coverage files found for upload. If you wish to only upload the specified files, please consider using --disable-search to disable uploading other files.", type=click.Path(path_type=pathlib.Path), multiple=True, @@ -155,6 +158,12 @@ def _turn_env_vars_into_dict(ctx, params, value): is_flag=True, help="Raise no excpetions when no coverage reports found.", ), + click.option( + "--report-type", + help="The type of the file to upload, coverage by default. Possible values are: testing, coverage.", + default="coverage", + type=click.Choice(["coverage", "test_results"]), + ), ] @@ -179,9 +188,9 @@ def do_upload( flags: typing.List[str], name: typing.Optional[str], network_root_folder: pathlib.Path, - coverage_files_search_root_folder: pathlib.Path, - coverage_files_search_exclude_folders: typing.List[pathlib.Path], - coverage_files_search_explicitly_listed_files: typing.List[pathlib.Path], + files_search_root_folder: pathlib.Path, + files_search_exclude_folders: typing.List[pathlib.Path], + files_search_explicitly_listed_files: typing.List[pathlib.Path], disable_search: bool, disable_file_fixes: bool, token: typing.Optional[str], @@ -194,6 +203,7 @@ def do_upload( dry_run: bool, git_service: typing.Optional[str], handle_no_reports_found: bool, + report_type: str, ): versioning_system = ctx.obj["versioning_system"] codecov_yaml = ctx.obj["codecov_yaml"] or {} @@ -204,6 +214,7 @@ def do_upload( "Starting upload processing", extra=dict( extra_log_attributes=dict( + upload_file_type=report_type, commit_sha=commit_sha, report_code=report_code, build_code=build_code, @@ -213,9 +224,9 @@ def do_upload( flags=flags, name=name, network_root_folder=network_root_folder, - coverage_files_search_root_folder=coverage_files_search_root_folder, - coverage_files_search_exclude_folders=coverage_files_search_exclude_folders, - coverage_files_search_explicitly_listed_files=coverage_files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, plugin_names=plugin_names, token=token, branch=branch, @@ -233,6 +244,7 @@ def do_upload( cli_config, versioning_system, ci_adapter, + upload_file_type=report_type, commit_sha=commit_sha, report_code=report_code, build_code=build_code, @@ -242,13 +254,9 @@ def do_upload( flags=flags, name=name, network_root_folder=network_root_folder, - coverage_files_search_root_folder=coverage_files_search_root_folder, - coverage_files_search_exclude_folders=list( - coverage_files_search_exclude_folders - ), - coverage_files_search_explicitly_listed_files=list( - coverage_files_search_explicitly_listed_files - ), + files_search_root_folder=files_search_root_folder, + files_search_exclude_folders=list(files_search_exclude_folders), + files_search_explicitly_listed_files=list(files_search_explicitly_listed_files), plugin_names=plugin_names, token=token, branch=branch, diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index b8270b79..20b67352 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -32,9 +32,9 @@ def upload_process( flags: typing.List[str], name: typing.Optional[str], network_root_folder: pathlib.Path, - coverage_files_search_root_folder: pathlib.Path, - coverage_files_search_exclude_folders: typing.List[pathlib.Path], - coverage_files_search_explicitly_listed_files: typing.List[pathlib.Path], + files_search_root_folder: pathlib.Path, + files_search_exclude_folders: typing.List[pathlib.Path], + files_search_explicitly_listed_files: typing.List[pathlib.Path], disable_search: bool, disable_file_fixes: bool, token: typing.Optional[str], @@ -48,6 +48,7 @@ def upload_process( git_service: typing.Optional[str], parent_sha: typing.Optional[str], handle_no_reports_found: bool, + report_type: str, ): logger.debug( "Starting upload process", @@ -62,9 +63,9 @@ def upload_process( flags=flags, name=name, network_root_folder=network_root_folder, - coverage_files_search_root_folder=coverage_files_search_root_folder, - coverage_files_search_exclude_folders=coverage_files_search_exclude_folders, - coverage_files_search_explicitly_listed_files=coverage_files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, plugin_names=plugin_names, token=token, branch=branch, @@ -90,15 +91,16 @@ def upload_process( git_service=git_service, fail_on_error=True, ) - ctx.invoke( - create_report, - token=token, - code=report_code, - fail_on_error=True, - commit_sha=commit_sha, - slug=slug, - git_service=git_service, - ) + if report_type == "coverage": + ctx.invoke( + create_report, + token=token, + code=report_code, + fail_on_error=True, + commit_sha=commit_sha, + slug=slug, + git_service=git_service, + ) ctx.invoke( do_upload, commit_sha=commit_sha, @@ -110,9 +112,9 @@ def upload_process( flags=flags, name=name, network_root_folder=network_root_folder, - coverage_files_search_root_folder=coverage_files_search_root_folder, - coverage_files_search_exclude_folders=coverage_files_search_exclude_folders, - coverage_files_search_explicitly_listed_files=coverage_files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, disable_search=disable_search, token=token, plugin_names=plugin_names, @@ -125,4 +127,5 @@ def upload_process( git_service=git_service, handle_no_reports_found=handle_no_reports_found, disable_file_fixes=disable_file_fixes, + report_type=report_type, ) diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index c012beb4..30960ae6 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -9,7 +9,7 @@ from codecov_cli.helpers.request import log_warnings_and_errors_if_any from codecov_cli.helpers.versioning_systems import VersioningSystemInterface from codecov_cli.plugins import select_preparation_plugins -from codecov_cli.services.upload.coverage_file_finder import select_coverage_file_finder +from codecov_cli.services.upload.file_finder import select_file_finder from codecov_cli.services.upload.legacy_upload_sender import LegacyUploadSender from codecov_cli.services.upload.network_finder import select_network_finder from codecov_cli.services.upload.upload_collector import UploadCollector @@ -34,14 +34,15 @@ def do_upload_logic( flags: typing.List[str], name: typing.Optional[str], network_root_folder: Path, - coverage_files_search_root_folder: Path, - coverage_files_search_exclude_folders: typing.List[Path], - coverage_files_search_explicitly_listed_files: typing.List[Path], + files_search_root_folder: Path, + files_search_exclude_folders: typing.List[Path], + files_search_explicitly_listed_files: typing.List[Path], plugin_names: typing.List[str], token: str, branch: typing.Optional[str], slug: typing.Optional[str], pull_request_number: typing.Optional[str], + upload_file_type: str = "coverage", use_legacy_uploader: bool = False, fail_on_error: bool = False, dry_run: bool = False, @@ -51,19 +52,23 @@ def do_upload_logic( handle_no_reports_found: bool = False, disable_file_fixes: bool = False, ): - preparation_plugins = select_preparation_plugins(cli_config, plugin_names) - coverage_file_selector = select_coverage_file_finder( - coverage_files_search_root_folder, - coverage_files_search_exclude_folders, - coverage_files_search_explicitly_listed_files, + if upload_file_type == "coverage": + preparation_plugins = select_preparation_plugins(cli_config, plugin_names) + elif upload_file_type == "test_results": + preparation_plugins = [] + file_selector = select_file_finder( + files_search_root_folder, + files_search_exclude_folders, + files_search_explicitly_listed_files, disable_search, + upload_file_type, ) network_finder = select_network_finder(versioning_system) collector = UploadCollector( - preparation_plugins, network_finder, coverage_file_selector, disable_file_fixes + preparation_plugins, network_finder, file_selector, disable_file_fixes ) try: - upload_data = collector.generate_upload_data() + upload_data = collector.generate_upload_data(upload_file_type) except click.ClickException as exp: if handle_no_reports_found: logger.info( @@ -103,6 +108,7 @@ def do_upload_logic( token, env_vars, report_code, + upload_file_type, name, branch, slug, diff --git a/codecov_cli/services/upload/coverage_file_finder.py b/codecov_cli/services/upload/file_finder.py similarity index 77% rename from codecov_cli/services/upload/coverage_file_finder.py rename to codecov_cli/services/upload/file_finder.py index 555df796..e98517de 100644 --- a/codecov_cli/services/upload/coverage_file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -35,6 +35,9 @@ "test_cov.xml", ] +test_results_files_patterns = [ + "*junit*", +] coverage_files_excluded_patterns = [ "*.am", @@ -134,6 +137,10 @@ "*.zip", ] +test_results_files_excluded_patterns = ( + coverage_files_patterns + coverage_files_excluded_patterns +) + default_folders_to_ignore = [ "vendor", @@ -170,49 +177,53 @@ ] -class CoverageFileFinder(object): +class FileFinder(object): def __init__( self, project_root: Path = None, folders_to_ignore: typing.List[str] = None, explicitly_listed_files: typing.List[Path] = None, disable_search: bool = False, + report_type: str = "coverage", ): self.project_root = project_root or Path(os.getcwd()) self.folders_to_ignore = folders_to_ignore or [] self.explicitly_listed_files = explicitly_listed_files or None self.disable_search = disable_search + self.report_type = report_type - def find_coverage_files(self) -> typing.List[UploadCollectionResultFile]: - regex_patterns_to_exclude = globs_to_regex(coverage_files_excluded_patterns) - coverage_files_paths = [] - user_coverage_files_paths = [] + def find_files(self) -> typing.List[UploadCollectionResultFile]: + if self.report_type == "coverage": + files_excluded_patterns = coverage_files_excluded_patterns + files_patterns = coverage_files_patterns + elif self.report_type == "test_results": + files_excluded_patterns = test_results_files_excluded_patterns + files_patterns = test_results_files_patterns + regex_patterns_to_exclude = globs_to_regex(files_excluded_patterns) + files_paths = [] + user_files_paths = [] if self.explicitly_listed_files: - user_coverage_files_paths = self.get_user_specified_coverage_files( - regex_patterns_to_exclude - ) + user_files_paths = self.get_user_specified_files(regex_patterns_to_exclude) if not self.disable_search: - regex_patterns_to_include = globs_to_regex(coverage_files_patterns) - coverage_files_paths = search_files( + regex_patterns_to_include = globs_to_regex(files_patterns) + files_paths = search_files( self.project_root, default_folders_to_ignore + self.folders_to_ignore, filename_include_regex=regex_patterns_to_include, filename_exclude_regex=regex_patterns_to_exclude, ) result_files = [ - UploadCollectionResultFile(path) - for path in coverage_files_paths - if coverage_files_paths + UploadCollectionResultFile(path) for path in files_paths if files_paths ] user_result_files = [ UploadCollectionResultFile(path) - for path in user_coverage_files_paths - if user_coverage_files_paths + for path in user_files_paths + if user_files_paths ] return list(set(result_files + user_result_files)) - def get_user_specified_coverage_files(self, regex_patterns_to_exclude): + def get_user_specified_files(self, regex_patterns_to_exclude): user_filenames_to_include = [] files_excluded_but_user_includes = [] for file in self.explicitly_listed_files: @@ -230,7 +241,7 @@ def get_user_specified_coverage_files(self, regex_patterns_to_exclude): multipart_include_regex = globs_to_regex( [str(path.resolve()) for path in self.explicitly_listed_files] ) - user_coverage_files_paths = list( + user_files_paths = list( search_files( self.project_root, default_folders_to_ignore + self.folders_to_ignore, @@ -241,7 +252,7 @@ def get_user_specified_coverage_files(self, regex_patterns_to_exclude): ) not_found_files = [] for filepath in self.explicitly_listed_files: - if filepath.resolve() not in user_coverage_files_paths: + if filepath.resolve() not in user_files_paths: not_found_files.append(filepath) if not_found_files: @@ -250,15 +261,20 @@ def get_user_specified_coverage_files(self, regex_patterns_to_exclude): extra=dict(extra_log_attributes=dict(not_found_files=not_found_files)), ) - return user_coverage_files_paths + return user_files_paths -def select_coverage_file_finder( - root_folder_to_search, folders_to_ignore, explicitly_listed_files, disable_search +def select_file_finder( + root_folder_to_search, + folders_to_ignore, + explicitly_listed_files, + disable_search, + report_type="coverage", ): - return CoverageFileFinder( + return FileFinder( root_folder_to_search, folders_to_ignore, explicitly_listed_files, disable_search, + report_type, ) diff --git a/codecov_cli/services/upload/legacy_upload_sender.py b/codecov_cli/services/upload/legacy_upload_sender.py index da91ae7c..283b204d 100644 --- a/codecov_cli/services/upload/legacy_upload_sender.py +++ b/codecov_cli/services/upload/legacy_upload_sender.py @@ -39,6 +39,7 @@ def send_upload_data( token: str, env_vars: typing.Dict[str, str], report_code: str = None, + upload_file_type: str = None, name: typing.Optional[str] = None, branch: typing.Optional[str] = None, slug: typing.Optional[str] = None, @@ -51,7 +52,6 @@ def send_upload_data( git_service: typing.Optional[str] = None, enterprise_url: typing.Optional[str] = None, ) -> UploadSendingResult: - params = { "package": f"codecov-cli/{codecov_cli_version}", "commit": commit_sha, @@ -116,9 +116,7 @@ def _generate_network_section(self, upload_data: UploadCollectionResult) -> byte return network_files_section.encode() + b"<<<<<< network\n" def _generate_coverage_files_section(self, upload_data: UploadCollectionResult): - return b"".join( - self._format_coverage_file(file) for file in upload_data.coverage_files - ) + return b"".join(self._format_coverage_file(file) for file in upload_data.files) def _format_coverage_file(self, file: UploadCollectionResultFile) -> bytes: header = b"# path=" + file.get_filename() + b"\n" diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 0668d9d6..282c0e99 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -8,7 +8,7 @@ import click -from codecov_cli.services.upload.coverage_file_finder import CoverageFileFinder +from codecov_cli.services.upload.file_finder import FileFinder from codecov_cli.services.upload.network_finder import NetworkFinder from codecov_cli.types import ( PreparationPluginInterface, @@ -28,12 +28,12 @@ def __init__( self, preparation_plugins: typing.List[PreparationPluginInterface], network_finder: NetworkFinder, - coverage_file_finder: CoverageFileFinder, + file_finder: FileFinder, disable_file_fixes: bool = False, ): self.preparation_plugins = preparation_plugins self.network_finder = network_finder - self.coverage_file_finder = coverage_file_finder + self.file_finder = file_finder self.disable_file_fixes = disable_file_fixes def _produce_file_fixes_for_network( @@ -144,25 +144,27 @@ def _get_file_fixes( path, fixed_lines_without_reason, fixed_lines_with_reason, eof ) - def generate_upload_data(self) -> UploadCollectionResult: + def generate_upload_data(self, report_type="coverage") -> UploadCollectionResult: for prep in self.preparation_plugins: logger.debug(f"Running preparation plugin: {type(prep)}") prep.run_preparation(self) logger.debug("Collecting relevant files") network = self.network_finder.find_files() - coverage_files = self.coverage_file_finder.find_coverage_files() - logger.info(f"Found {len(coverage_files)} coverage files to upload") - if not coverage_files: + files = self.file_finder.find_files() + logger.info(f"Found {len(files)} {report_type} files to upload") + if not files: raise click.ClickException( click.style( - "No coverage reports found. Please make sure you're generating reports successfully.", + f"No {report_type} reports found. Please make sure you're generating reports successfully.", fg="red", ) ) - for file in coverage_files: + for file in files: logger.info(f"> {file}") return UploadCollectionResult( network=network, - coverage_files=coverage_files, - file_fixes=self._produce_file_fixes_for_network(network), + files=files, + file_fixes=self._produce_file_fixes_for_network(network) + if report_type == "coverage" + else [], ) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index f500f71b..f1399d48 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -31,6 +31,7 @@ def send_upload_data( token: str, env_vars: typing.Dict[str, str], report_code: str, + upload_file_type: str = "coverage", name: typing.Optional[str] = None, branch: typing.Optional[str] = None, slug: typing.Optional[str] = None, @@ -66,9 +67,19 @@ def send_upload_data( headers = get_token_header_or_fail(token) encoded_slug = encode_slug(slug) upload_url = enterprise_url or CODECOV_API_URL - url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/uploads" + url, data = self.get_url_and_possibly_update_data( + data, + upload_file_type, + upload_url, + git_service, + encoded_slug, + commit_sha, + report_code, + ) # Data that goes to storage - reports_payload = self._generate_payload(upload_data, env_vars) + reports_payload = self._generate_payload( + upload_data, env_vars, upload_file_type + ) logger.debug("Sending upload request to Codecov") resp_from_codecov = send_post_request( @@ -93,18 +104,26 @@ def send_upload_data( return resp_from_storage def _generate_payload( - self, upload_data: UploadCollectionResult, env_vars: typing.Dict[str, str] + self, + upload_data: UploadCollectionResult, + env_vars: typing.Dict[str, str], + upload_file_type="coverage", ) -> bytes: network_files = upload_data.network - payload = { - "report_fixes": { - "format": "legacy", - "value": self._get_file_fixers(upload_data), - }, - "network_files": network_files if network_files is not None else [], - "coverage_files": self._get_coverage_files(upload_data), - "metadata": {}, - } + if upload_file_type == "coverage": + payload = { + "report_fixes": { + "format": "legacy", + "value": self._get_file_fixers(upload_data), + }, + "network_files": network_files if network_files is not None else [], + "coverage_files": self._get_files(upload_data), + "metadata": {}, + } + elif upload_file_type == "test_results": + payload = { + "test_results_files": self._get_files(upload_data), + } json_data = json.dumps(payload) return json_data.encode() @@ -137,10 +156,10 @@ def _get_file_fixers( return file_fixers - def _get_coverage_files(self, upload_data: UploadCollectionResult): - return [self._format_coverage_file(file) for file in upload_data.coverage_files] + def _get_files(self, upload_data: UploadCollectionResult): + return [self._format_file(file) for file in upload_data.files] - def _format_coverage_file(self, file: UploadCollectionResultFile): + def _format_file(self, file: UploadCollectionResultFile): format, formatted_content = self._get_format_info(file) return { "filename": file.get_filename().decode(), @@ -155,3 +174,23 @@ def _get_format_info(self, file: UploadCollectionResultFile): base64.b64encode(zlib.compress((file.get_content()))) ).decode() return format, formatted_content + + def get_url_and_possibly_update_data( + self, + data, + report_type, + upload_url, + git_service, + encoded_slug, + commit_sha, + report_code, + ): + if report_type == "coverage": + url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/uploads" + elif report_type == "test_results": + data["slug"] = encoded_slug + data["commit"] = commit_sha + data["service"] = git_service + url = f"{upload_url}/upload/test_results/v1" + + return url, data diff --git a/codecov_cli/types.py b/codecov_cli/types.py index 95f9f759..d9405f2e 100644 --- a/codecov_cli/types.py +++ b/codecov_cli/types.py @@ -38,9 +38,9 @@ class UploadCollectionResultFileFixer(object): @dataclass class UploadCollectionResult(object): - __slots__ = ["network", "coverage_files", "file_fixes"] + __slots__ = ["network", "files", "file_fixes"] network: typing.List[str] - coverage_files: typing.List[UploadCollectionResultFile] + files: typing.List[UploadCollectionResultFile] file_fixes: typing.List[UploadCollectionResultFileFixer] diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 7476588b..67e88607 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -78,12 +78,12 @@ def test_upload_process_options(mocker): " --network-root-folder PATH Root folder from which to consider paths on", " the network section [default: (Current", " working directory)]", - " -s, --dir, --coverage-files-search-root-folder PATH", + " -s, --dir, --coverage-files-search-root-folder, --files-search-root-folder PATH", " Folder where to search for coverage files", " [default: (Current Working Directory)]", - " --exclude, --coverage-files-search-exclude-folder PATH", + " --exclude, --coverage-files-search-exclude-folder, --files-search-exclude-folder PATH", " Folders to exclude from search", - " -f, --file, --coverage-files-search-direct-file PATH", + " -f, --file, --coverage-files-search-direct-file, --files-search-direct-file PATH", " Explicit files to upload. These will be added", " to the coverage files found for upload. If you", " wish to only upload the specified files,", @@ -114,6 +114,10 @@ def test_upload_process_options(mocker): " Use the legacy upload endpoint", " --handle-no-reports-found Raise no excpetions when no coverage reports", " found.", + " --report-type [coverage|test_results]", + " The type of the file to upload, coverage by", + " default. Possible values are: testing,", + " coverage.", " --parent-sha TEXT SHA (with 40 chars) of what should be the", " parent of this commit", " -h, --help Show this message and exit.", diff --git a/tests/helpers/test_legacy_upload_sender.py b/tests/helpers/test_legacy_upload_sender.py index fdfb9229..beb4c79c 100644 --- a/tests/helpers/test_legacy_upload_sender.py +++ b/tests/helpers/test_legacy_upload_sender.py @@ -166,7 +166,11 @@ def test_upload_sender_http_error_with_invalid_sha( mocked_legacy_upload_endpoint.status = 400 sender = LegacyUploadSender().send_upload_data( - upload_collection, random_sha, random_token, {}, **named_upload_data + upload_collection, + random_sha, + random_token, + {}, + **named_upload_data, ) assert sender.error is not None @@ -279,7 +283,6 @@ def test_format_coverage_file(self, mocker): ) def test_generate_coverage_files_section(self, mocker): - mocker.patch( "codecov_cli.services.upload.LegacyUploadSender._format_coverage_file", side_effect=lambda file_bytes: file_bytes, diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index ec54d3b3..1d46dbfa 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -15,6 +15,22 @@ random_token = "f359afb9-8a2a-42ab-a448-c3d267ff495b" random_sha = "845548c6b95223f12e8317a1820705f64beaf69e" named_upload_data = { + "upload_file_type": "coverage", + "report_code": "report_code", + "env_vars": {}, + "name": "name", + "branch": "branch", + "slug": "org/repo", + "pull_request_number": "pr", + "build_code": "build_code", + "build_url": "build_url", + "job_code": "job_code", + "flags": "flags", + "ci_service": "ci_service", + "git_service": "github", +} +test_results_named_upload_data = { + "upload_file_type": "test_results", "report_code": "report_code", "env_vars": {}, "name": "name", @@ -60,6 +76,20 @@ def mocked_legacy_upload_endpoint(mocked_responses): yield resp +@pytest.fixture +def mocked_test_results_endpoint(mocked_responses): + resp = responses.Response( + responses.POST, + f"https://api.codecov.io/upload/test_results/v1", + status=200, + json={ + "raw_upload_location": "https://puturl.com", + }, + ) + mocked_responses.add(resp) + yield resp + + @pytest.fixture def mocked_storage_server(mocked_responses): resp = responses.Response(responses.PUT, "https://puturl.com", status=200) @@ -159,6 +189,39 @@ def test_upload_sender_post_called_with_right_parameters( post_req_made.headers.items() >= headers.items() ) # test dict is a subset of the other + def test_upload_sender_post_called_with_right_parameters_test_results( + self, mocked_responses, mocked_test_results_endpoint, mocked_storage_server + ): + headers = {"Authorization": f"token {random_token}"} + + mocked_legacy_upload_endpoint.match = [ + matchers.json_params_matcher(request_data), + matchers.header_matcher(headers), + ] + + sending_result = UploadSender().send_upload_data( + upload_collection, + random_sha, + random_token, + **test_results_named_upload_data, + ) + assert sending_result.error is None + assert sending_result.warnings == [] + + assert len(mocked_responses.calls) == 2 + + post_req_made = mocked_responses.calls[0].request + response = json.loads(mocked_responses.calls[0].response.text) + assert response.get("raw_upload_location") == "https://puturl.com" + assert post_req_made.url == "https://api.codecov.io/upload/test_results/v1" + assert ( + post_req_made.headers.items() >= headers.items() + ) # test dict is a subset of the other + + put_req_made = mocked_responses.calls[1].request + assert put_req_made.url == "https://puturl.com/" + assert "test_results_files" in put_req_made.body.decode("utf-8") + def test_upload_sender_post_called_with_right_parameters_tokenless( self, mocked_responses, @@ -369,9 +432,7 @@ def test_coverage_file_format(self, mocker, mocked_coverage_file): "codecov_cli.services.upload.upload_sender.UploadSender._get_format_info", return_value=("base64+compressed", "encoded_file_data"), ) - json_formatted_coverage_file = UploadSender()._format_coverage_file( - mocked_coverage_file - ) + json_formatted_coverage_file = UploadSender()._format_file(mocked_coverage_file) print(json_formatted_coverage_file["data"]) assert json_formatted_coverage_file == { "filename": mocked_coverage_file.get_filename().decode(), diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index dd83a8a4..953e6873 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -2,17 +2,17 @@ import unittest from pathlib import Path -from codecov_cli.services.upload.coverage_file_finder import CoverageFileFinder +from codecov_cli.services.upload.file_finder import FileFinder from codecov_cli.types import UploadCollectionResultFile class TestCoverageFileFinder(object): def test_find_coverage_files_mocked_search_files(self, mocker): mocker.patch( - "codecov_cli.services.upload.coverage_file_finder.search_files", + "codecov_cli.services.upload.file_finder.search_files", return_value=[], ) - assert CoverageFileFinder().find_coverage_files() == [] + assert FileFinder().find_files() == [] coverage_files_paths = [ Path("a/b.txt"), @@ -20,7 +20,7 @@ def test_find_coverage_files_mocked_search_files(self, mocker): ] mocker.patch( - "codecov_cli.services.upload.coverage_file_finder.search_files", + "codecov_cli.services.upload.file_finder.search_files", return_value=coverage_files_paths, ) @@ -32,7 +32,7 @@ def test_find_coverage_files_mocked_search_files(self, mocker): expected_paths = sorted([file.get_filename() for file in expected]) actual_paths = sorted( - [file.get_filename() for file in CoverageFileFinder().find_coverage_files()] + [file.get_filename() for file in FileFinder().find_files()] ) assert expected_paths == actual_paths @@ -87,12 +87,69 @@ def test_find_coverage_files(self, tmp_path): expected = { UploadCollectionResultFile((tmp_path / file)) for file in should_find } - actual = set(CoverageFileFinder(tmp_path).find_coverage_files()) + actual = set(FileFinder(tmp_path).find_files()) assert actual == expected extra = tmp_path / "sub" / "nosetests.xml" extra.touch() - actual = set(CoverageFileFinder(tmp_path).find_coverage_files()) + actual = set(FileFinder(tmp_path).find_files()) + assert actual - expected == {UploadCollectionResultFile(extra)} + + def test_find_coverage_files_test_results(self, tmp_path): + (tmp_path / "sub").mkdir() + (tmp_path / "sub" / "subsub").mkdir() + (tmp_path / "node_modules").mkdir() + + should_find = ["junit.xml", "abc.junit.xml", "sub/junit.xml"] + + should_ignore = [ + "abc.codecov.exe", + "sub/abc.codecov.exe", + "codecov.exe", + "__pycache__", + "sub/subsub/__pycache__", + ".gitignore", + "a.sql", + "a.csv", + ".abc-coveragerc", + ".coverage-xyz", + "sub/scoverage.measurements.xyz", + "sub/test_abcd_coverage.txt", + "test-result-ff-codecoverage.json", + "node_modules/abc-coverage.cov", + "abc-coverage.cov", + "coverage-abc.abc", + "sub/coverage-abc.abc", + "sub/subsub/coverage-abc.abc", + "coverage.abc", + "jacocoxyz.xml", + "sub/jacocoxyz.xml", + "codecov.abc", + "sub/subsub/codecov.abc", + "xyz.codecov.abc", + "sub/xyz.codecov.abc", + "sub/subsub/xyz.codecov.abc", + "cover.out", + "abc.gcov", + "sub/abc.gcov", + "sub/subsub/abc.gcov", + ] + + for filename in should_find: + (tmp_path / filename).touch() + + for filename in should_ignore: + (tmp_path / filename).touch() + + expected = { + UploadCollectionResultFile((tmp_path / file)) for file in should_find + } + actual = set(FileFinder(tmp_path, report_type="test_results").find_files()) + assert actual == expected + + extra = tmp_path / "sub" / "nosetests.junit.xml" + extra.touch() + actual = set(FileFinder(tmp_path, report_type="test_results").find_files()) assert actual - expected == {UploadCollectionResultFile(extra)} @@ -106,7 +163,7 @@ def setUp(self): self.project_root / "subdirectory" / "another_file.abc", ] self.disable_search = False - self.coverage_file_finder = CoverageFileFinder( + self.coverage_file_finder = FileFinder( self.project_root, self.folders_to_ignore, self.explicitly_listed_files, @@ -128,10 +185,7 @@ def test_find_coverage_files_with_existing_files(self): file.touch() result = sorted( - [ - file.get_filename() - for file in self.coverage_file_finder.find_coverage_files() - ] + [file.get_filename() for file in self.coverage_file_finder.find_files()] ) expected = [ UploadCollectionResultFile(Path(f"{self.project_root}/coverage.xml")), @@ -143,7 +197,7 @@ def test_find_coverage_files_with_existing_files(self): self.assertEqual(result, expected_paths) def test_find_coverage_files_with_no_files(self): - result = self.coverage_file_finder.find_coverage_files() + result = self.coverage_file_finder.find_files() self.assertEqual(result, []) def test_find_coverage_files_with_disabled_search(self): @@ -163,10 +217,7 @@ def test_find_coverage_files_with_disabled_search(self): self.coverage_file_finder.disable_search = True result = sorted( - [ - file.get_filename() - for file in self.coverage_file_finder.find_coverage_files() - ] + [file.get_filename() for file in self.coverage_file_finder.find_files()] ) expected = [ @@ -192,10 +243,7 @@ def test_find_coverage_files_with_user_specified_files(self): file.touch() result = sorted( - [ - file.get_filename() - for file in self.coverage_file_finder.find_coverage_files() - ] + [file.get_filename() for file in self.coverage_file_finder.find_files()] ) expected = [ @@ -227,10 +275,7 @@ def test_find_coverage_files_with_user_specified_files_not_found(self): ) result = sorted( - [ - file.get_filename() - for file in self.coverage_file_finder.find_coverage_files() - ] + [file.get_filename() for file in self.coverage_file_finder.find_files()] ) expected = [ diff --git a/tests/services/upload/test_upload_collector.py b/tests/services/upload/test_upload_collector.py index 15279275..0df1c0bc 100644 --- a/tests/services/upload/test_upload_collector.py +++ b/tests/services/upload/test_upload_collector.py @@ -1,7 +1,11 @@ from pathlib import Path from unittest.mock import patch +from codecov_cli.helpers.versioning_systems import GitVersioningSystem +from codecov_cli.services.upload.file_finder import FileFinder +from codecov_cli.services.upload.network_finder import NetworkFinder from codecov_cli.services.upload.upload_collector import UploadCollector +from codecov_cli.types import UploadCollectionResultFile def test_fix_kt_files(): @@ -109,3 +113,64 @@ def test_fix_when_disabled_fixes(tmp_path): assert len(fixes) == 0 assert fixes == [] + + +def test_generate_upload_data(tmp_path): + (tmp_path / "sub").mkdir() + (tmp_path / "sub" / "subsub").mkdir() + (tmp_path / "node_modules").mkdir() + + should_find = [ + "abc-coverage.cov", + "coverage-abc.abc", + "sub/coverage-abc.abc", + "sub/subsub/coverage-abc.abc", + "coverage.abc", + "jacocoxyz.xml", + "sub/jacocoxyz.xml", + "codecov.abc", + "sub/subsub/codecov.abc", + "xyz.codecov.abc", + "sub/xyz.codecov.abc", + "sub/subsub/xyz.codecov.abc", + "cover.out", + "abc.gcov", + "sub/abc.gcov", + "sub/subsub/abc.gcov", + ] + + should_ignore = [ + "abc.codecov.exe", + "sub/abc.codecov.exe", + "codecov.exe", + "__pycache__", + "sub/subsub/__pycache__", + ".gitignore", + "a.sql", + "a.csv", + ".abc-coveragerc", + ".coverage-xyz", + "sub/scoverage.measurements.xyz", + "sub/test_abcd_coverage.txt", + "test-result-ff-codecoverage.json", + "node_modules/abc-coverage.cov", + ] + + for filename in should_find: + (tmp_path / filename).touch() + + for filename in should_ignore: + (tmp_path / filename).touch() + + file_finder = FileFinder(tmp_path) + + network_finder = NetworkFinder(GitVersioningSystem()) + + collector = UploadCollector([], network_finder, file_finder) + + res = collector.generate_upload_data() + + expected = {UploadCollectionResultFile(tmp_path / file) for file in should_find} + + for file in expected: + assert file in res.files diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index dfb6c4da..9f7dbf61 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -20,8 +20,8 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): mock_select_preparation_plugins = mocker.patch( "codecov_cli.services.upload.select_preparation_plugins" ) - mock_select_coverage_file_finder = mocker.patch( - "codecov_cli.services.upload.select_coverage_file_finder" + mock_select_file_finder = mocker.patch( + "codecov_cli.services.upload.select_file_finder" ) mock_select_network_finder = mocker.patch( "codecov_cli.services.upload.select_network_finder" @@ -47,6 +47,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): cli_config, versioning_system, ci_adapter, + upload_file_type="coverage", commit_sha="commit_sha", report_code="report_code", build_code="build_code", @@ -56,9 +57,9 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): flags=None, name="name", network_root_folder=None, - coverage_files_search_root_folder=None, - coverage_files_search_exclude_folders=None, - coverage_files_search_explicitly_listed_files=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], token="token", branch="branch", @@ -79,15 +80,16 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): mock_select_preparation_plugins.assert_called_with( cli_config, ["first_plugin", "another", "forth"] ) - mock_select_coverage_file_finder.assert_called_with(None, None, None, False) + mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with(versioning_system) - mock_generate_upload_data.assert_called_with() + mock_generate_upload_data.assert_called_with("coverage") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, "commit_sha", "token", None, "report_code", + "coverage", "name", "branch", "slug", @@ -106,8 +108,8 @@ def test_do_upload_logic_happy_path(mocker): mock_select_preparation_plugins = mocker.patch( "codecov_cli.services.upload.select_preparation_plugins" ) - mock_select_coverage_file_finder = mocker.patch( - "codecov_cli.services.upload.select_coverage_file_finder" + mock_select_file_finder = mocker.patch( + "codecov_cli.services.upload.select_file_finder" ) mock_select_network_finder = mocker.patch( "codecov_cli.services.upload.select_network_finder" @@ -133,6 +135,7 @@ def test_do_upload_logic_happy_path(mocker): cli_config, versioning_system, ci_adapter, + upload_file_type="coverage", commit_sha="commit_sha", report_code="report_code", build_code="build_code", @@ -142,9 +145,9 @@ def test_do_upload_logic_happy_path(mocker): flags=None, name="name", network_root_folder=None, - coverage_files_search_root_folder=None, - coverage_files_search_exclude_folders=None, - coverage_files_search_explicitly_listed_files=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], token="token", branch="branch", @@ -164,15 +167,16 @@ def test_do_upload_logic_happy_path(mocker): mock_select_preparation_plugins.assert_called_with( cli_config, ["first_plugin", "another", "forth"] ) - mock_select_coverage_file_finder.assert_called_with(None, None, None, False) + mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with(versioning_system) - mock_generate_upload_data.assert_called_with() + mock_generate_upload_data.assert_called_with("coverage") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, "commit_sha", "token", None, "report_code", + "coverage", "name", "branch", "slug", @@ -191,8 +195,8 @@ def test_do_upload_logic_dry_run(mocker): mock_select_preparation_plugins = mocker.patch( "codecov_cli.services.upload.select_preparation_plugins" ) - mock_select_coverage_file_finder = mocker.patch( - "codecov_cli.services.upload.select_coverage_file_finder" + mock_select_file_finder = mocker.patch( + "codecov_cli.services.upload.select_file_finder" ) mock_select_network_finder = mocker.patch( "codecov_cli.services.upload.select_network_finder" @@ -214,6 +218,7 @@ def test_do_upload_logic_dry_run(mocker): cli_config, versioning_system, ci_adapter, + upload_file_type="coverage", commit_sha="commit_sha", report_code="report_code", build_code="build_code", @@ -223,9 +228,9 @@ def test_do_upload_logic_dry_run(mocker): flags=None, name="name", network_root_folder=None, - coverage_files_search_root_folder=None, - coverage_files_search_exclude_folders=None, - coverage_files_search_explicitly_listed_files=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], token="token", branch="branch", @@ -236,7 +241,7 @@ def test_do_upload_logic_dry_run(mocker): enterprise_url=None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) - mock_select_coverage_file_finder.assert_called_with(None, None, None, False) + mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with(versioning_system) assert mock_generate_upload_data.call_count == 1 assert mock_send_upload_data.call_count == 0 @@ -257,7 +262,7 @@ def test_do_upload_logic_dry_run(mocker): def test_do_upload_logic_verbose(mocker, use_verbose_option): mocker.patch("codecov_cli.services.upload.select_preparation_plugins") - mocker.patch("codecov_cli.services.upload.select_coverage_file_finder") + mocker.patch("codecov_cli.services.upload.select_file_finder") mocker.patch("codecov_cli.services.upload.select_network_finder") mocker.patch.object(UploadCollector, "generate_upload_data") mocker.patch.object( @@ -274,6 +279,7 @@ def test_do_upload_logic_verbose(mocker, use_verbose_option): cli_config, versioning_system, ci_adapter, + upload_file_type="coverage", commit_sha="commit_sha", report_code="report_code", build_code="build_code", @@ -283,9 +289,9 @@ def test_do_upload_logic_verbose(mocker, use_verbose_option): flags=None, name="name", network_root_folder=None, - coverage_files_search_root_folder=None, - coverage_files_search_exclude_folders=None, - coverage_files_search_explicitly_listed_files=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], token="token", branch="branch", @@ -321,8 +327,8 @@ def test_do_upload_no_cov_reports_found(mocker): mock_select_preparation_plugins = mocker.patch( "codecov_cli.services.upload.select_preparation_plugins" ) - mock_select_coverage_file_finder = mocker.patch( - "codecov_cli.services.upload.select_coverage_file_finder", + mock_select_file_finder = mocker.patch( + "codecov_cli.services.upload.select_file_finder", ) mock_select_network_finder = mocker.patch( "codecov_cli.services.upload.select_network_finder" @@ -348,6 +354,7 @@ def side_effect(*args, **kwargs): cli_config, versioning_system, ci_adapter, + upload_file_type="coverage", commit_sha="commit_sha", report_code="report_code", build_code="build_code", @@ -357,9 +364,9 @@ def side_effect(*args, **kwargs): flags=None, name="name", network_root_folder=None, - coverage_files_search_root_folder=None, - coverage_files_search_exclude_folders=None, - coverage_files_search_explicitly_listed_files=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], token="token", branch="branch", @@ -385,9 +392,9 @@ def side_effect(*args, **kwargs): mock_select_preparation_plugins.assert_called_with( cli_config, ["first_plugin", "another", "forth"] ) - mock_select_coverage_file_finder.assert_called_with(None, None, None, False) + mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with(versioning_system) - mock_generate_upload_data.assert_called_with() + mock_generate_upload_data.assert_called_with("coverage") mock_upload_completion_call.assert_called_with( commit_sha="commit_sha", slug="slug", @@ -402,8 +409,8 @@ def test_do_upload_rase_no_cov_reports_found_error(mocker): mock_select_preparation_plugins = mocker.patch( "codecov_cli.services.upload.select_preparation_plugins" ) - mock_select_coverage_file_finder = mocker.patch( - "codecov_cli.services.upload.select_coverage_file_finder", + mock_select_file_finder = mocker.patch( + "codecov_cli.services.upload.select_file_finder", ) mock_select_network_finder = mocker.patch( "codecov_cli.services.upload.select_network_finder" @@ -428,6 +435,7 @@ def side_effect(*args, **kwargs): cli_config, versioning_system, ci_adapter, + upload_file_type="coverage", commit_sha="commit_sha", report_code="report_code", build_code="build_code", @@ -437,9 +445,9 @@ def side_effect(*args, **kwargs): flags=None, name="name", network_root_folder=None, - coverage_files_search_root_folder=None, - coverage_files_search_exclude_folders=None, - coverage_files_search_explicitly_listed_files=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], token="token", branch="branch", @@ -456,6 +464,91 @@ def side_effect(*args, **kwargs): mock_select_preparation_plugins.assert_called_with( cli_config, ["first_plugin", "another", "forth"] ) - mock_select_coverage_file_finder.assert_called_with(None, None, None, False) + mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with(versioning_system) - mock_generate_upload_data.assert_called_with() + mock_generate_upload_data.assert_called_with("coverage") + + +def test_do_upload_logic_happy_path_test_results(mocker): + mock_select_preparation_plugins = mocker.patch( + "codecov_cli.services.upload.select_preparation_plugins" + ) + mock_select_file_finder = mocker.patch( + "codecov_cli.services.upload.select_file_finder" + ) + mock_select_network_finder = mocker.patch( + "codecov_cli.services.upload.select_network_finder" + ) + mock_generate_upload_data = mocker.patch.object( + UploadCollector, "generate_upload_data" + ) + mock_send_upload_data = mocker.patch.object( + UploadSender, + "send_upload_data", + return_value=UploadSendingResult( + error=None, + warnings=[UploadSendingResultWarning(message="somewarningmessage")], + ), + ) + cli_config = {} + versioning_system = mocker.MagicMock() + ci_adapter = mocker.MagicMock() + ci_adapter.get_fallback_value.return_value = "service" + runner = CliRunner() + with runner.isolation() as outstreams: + res = do_upload_logic( + cli_config, + versioning_system, + ci_adapter, + upload_file_type="test_results", + commit_sha="commit_sha", + report_code="report_code", + build_code="build_code", + build_url="build_url", + job_code="job_code", + env_vars=None, + flags=None, + name="name", + network_root_folder=None, + files_search_root_folder=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, + plugin_names=["first_plugin", "another", "forth"], + token="token", + branch="branch", + slug="slug", + pull_request_number="pr", + git_service="git_service", + enterprise_url=None, + ) + out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) + assert out_bytes == [ + ("info", "Process Upload complete"), + ("info", "Upload process had 1 warning"), + ("warning", "Warning 1: somewarningmessage"), + ] + + assert res == UploadSender.send_upload_data.return_value + mock_select_preparation_plugins.assert_not_called + mock_select_file_finder.assert_called_with(None, None, None, False, "test_results") + mock_select_network_finder.assert_called_with(versioning_system) + mock_generate_upload_data.assert_called_with("test_results") + mock_send_upload_data.assert_called_with( + mock_generate_upload_data.return_value, + "commit_sha", + "token", + None, + "report_code", + "test_results", + "name", + "branch", + "slug", + "pr", + "build_code", + "build_url", + "job_code", + None, + "service", + "git_service", + None, + ) From ef198101fbdefbacbc37ba378946071887daf887 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:38:29 -0500 Subject: [PATCH 042/128] build: don't run steps that req secrets if fork (#359) * build: don't run steps that req secrets if fork we don't want to run the steps in the CI workflow that require access to secrets so that they don't block the tests from running. * build: build-test-upload doesn' need codecov-startup Signed-off-by: joseph-sentry --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6edb5c81..17ea81c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: codecov-startup: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} steps: - uses: actions/checkout@v4 with: @@ -50,7 +51,6 @@ jobs: build-test-upload: runs-on: ubuntu-latest - needs: codecov-startup strategy: fail-fast: false matrix: @@ -78,12 +78,14 @@ jobs: run: | pytest --cov - name: Dogfooding codecov-cli + if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} run: | codecovcli do-upload --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} static-analysis: runs-on: ubuntu-latest needs: codecov-startup + if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} steps: - uses: actions/checkout@v4 with: @@ -107,6 +109,7 @@ jobs: label-analysis: runs-on: ubuntu-latest needs: static-analysis + if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} steps: - uses: actions/checkout@v4 with: From 019a55a5e3c4c54fe587e318658fe093c304db99 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:34:38 -0500 Subject: [PATCH 043/128] feat: add force flag to empty upload (#362) * feat: add force flag to empty upload * fix: change format of option for empty upload force Signed-off-by: joseph-sentry --- codecov_cli/commands/empty_upload.py | 4 +- codecov_cli/services/empty_upload/__init__.py | 6 ++- .../empty_upload/test_empty_upload.py | 38 +++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/codecov_cli/commands/empty_upload.py b/codecov_cli/commands/empty_upload.py index 7cdd5428..8cabec8b 100644 --- a/codecov_cli/commands/empty_upload.py +++ b/codecov_cli/commands/empty_upload.py @@ -12,11 +12,13 @@ @click.command() +@click.option("--force", is_flag=True, default=False) @global_options @click.pass_context def empty_upload( ctx, commit_sha: str, + force: bool, slug: typing.Optional[str], token: typing.Optional[str], git_service: typing.Optional[str], @@ -37,5 +39,5 @@ def empty_upload( ), ) return empty_upload_logic( - commit_sha, slug, token, git_service, enterprise_url, fail_on_error + commit_sha, slug, token, git_service, enterprise_url, fail_on_error, force ) diff --git a/codecov_cli/services/empty_upload/__init__.py b/codecov_cli/services/empty_upload/__init__.py index 2e3ce68d..57bb32b9 100644 --- a/codecov_cli/services/empty_upload/__init__.py +++ b/codecov_cli/services/empty_upload/__init__.py @@ -13,13 +13,15 @@ def empty_upload_logic( - commit_sha, slug, token, git_service, enterprise_url, fail_on_error + commit_sha, slug, token, git_service, enterprise_url, fail_on_error, should_force ): encoded_slug = encode_slug(slug) headers = get_token_header_or_fail(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/empty-upload" - sending_result = send_post_request(url=url, headers=headers) + sending_result = send_post_request( + url=url, headers=headers, data={"should_force": should_force} + ) log_warnings_and_errors_if_any(sending_result, "Empty Upload", fail_on_error) if sending_result.status_code == 200: response_json = json.loads(sending_result.text) diff --git a/tests/services/empty_upload/test_empty_upload.py b/tests/services/empty_upload/test_empty_upload.py index 7ad75933..cc714470 100644 --- a/tests/services/empty_upload/test_empty_upload.py +++ b/tests/services/empty_upload/test_empty_upload.py @@ -21,7 +21,7 @@ def test_empty_upload_with_warnings(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False + "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -50,7 +50,7 @@ def test_empty_upload_with_error(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False + "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) @@ -77,7 +77,7 @@ def test_empty_upload_200(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", token, "service", None, False + "commit_sha", "owner/repo", token, "service", None, False, False ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -96,10 +96,40 @@ def test_empty_upload_403(mocker): return_value=mocker.MagicMock(status_code=403, text="Permission denied"), ) token = uuid.uuid4() - res = empty_upload_logic("commit_sha", "owner/repo", token, "service", None, False) + res = empty_upload_logic( + "commit_sha", "owner/repo", token, "service", None, False, False + ) assert res.error == RequestError( code="HTTP Error 403", description="Permission denied", params={}, ) mocked_response.assert_called_once() + + +def test_empty_upload_force(mocker): + res = { + "result": "Force option was enabled. Triggering passing notifications.", + "non_ignored_files": [], + } + mocked_response = mocker.patch( + "codecov_cli.helpers.request.requests.post", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text=json.dumps(res) + ), + ) + token = uuid.uuid4() + runner = CliRunner() + with runner.isolation() as outstreams: + res = empty_upload_logic( + "commit_sha", "owner/repo", token, "service", None, False, True + ) + out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) + assert out_bytes == [ + ("info", "Process Empty Upload complete"), + ("info", "Force option was enabled. Triggering passing notifications."), + ("info", "Non ignored files []"), + ] + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_once() From a27c5738e97455fc53751b39ee760edf79ce0a4c Mon Sep 17 00:00:00 2001 From: matt-codecov <137832199+matt-codecov@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:49:22 -0800 Subject: [PATCH 044/128] fix: search acceptable paths for codecov yaml if not passed in (#368) * fix: search acceptable paths for codecov yaml if not passed in * add test case ensuring load_cli_config() calls _find_codecov_yamls --- codecov_cli/helpers/config.py | 63 ++++++++++++++++++---- codecov_cli/main.py | 4 +- samples/fake_project/.codecov.yaml | 0 samples/fake_project/.codecov.yml | 0 samples/fake_project/.github/.codecov.yaml | 0 samples/fake_project/.github/.codecov.yml | 0 samples/fake_project/.github/codecov.yaml | 0 samples/fake_project/.github/codecov.yml | 0 samples/fake_project/codecov.yaml | 0 samples/fake_project/codecov.yml | 5 ++ samples/fake_project/dev/.codecov.yaml | 0 samples/fake_project/dev/.codecov.yml | 0 samples/fake_project/dev/codecov.yaml | 0 samples/fake_project/dev/codecov.yml | 0 tests/helpers/test_config.py | 45 +++++++++++++++- 15 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 samples/fake_project/.codecov.yaml create mode 100644 samples/fake_project/.codecov.yml create mode 100644 samples/fake_project/.github/.codecov.yaml create mode 100644 samples/fake_project/.github/.codecov.yml create mode 100644 samples/fake_project/.github/codecov.yaml create mode 100644 samples/fake_project/.github/codecov.yml create mode 100644 samples/fake_project/codecov.yaml create mode 100644 samples/fake_project/codecov.yml create mode 100644 samples/fake_project/dev/.codecov.yaml create mode 100644 samples/fake_project/dev/.codecov.yml create mode 100644 samples/fake_project/dev/codecov.yaml create mode 100644 samples/fake_project/dev/codecov.yml diff --git a/codecov_cli/helpers/config.py b/codecov_cli/helpers/config.py index 1ae2ac03..e8941056 100644 --- a/codecov_cli/helpers/config.py +++ b/codecov_cli/helpers/config.py @@ -1,20 +1,65 @@ import logging import pathlib +import typing import yaml +from codecov_cli.helpers.versioning_systems import get_versioning_system + logger = logging.getLogger("codecovcli") CODECOV_API_URL = "https://api.codecov.io" LEGACY_CODECOV_API_URL = "https://codecov.io" +# Relative to the project root +CODECOV_YAML_RECOGNIZED_DIRECTORIES = [ + "", + ".github/", + "dev/", +] + +CODECOV_YAML_RECOGNIZED_FILENAMES = [ + "codecov.yml", + "codecov.yaml", + ".codecov.yml", + ".codecov.yaml", +] + + +def _find_codecov_yamls(): + vcs = get_versioning_system() + vcs_root = vcs.get_network_root() if vcs else None + project_root = vcs_root if vcs_root else pathlib.Path.cwd() + + yamls = [] + for directory in CODECOV_YAML_RECOGNIZED_DIRECTORIES: + dir_candidate = project_root / directory + if not dir_candidate.exists() or not dir_candidate.is_dir(): + continue + + for filename in CODECOV_YAML_RECOGNIZED_FILENAMES: + file_candidate = dir_candidate / filename + if file_candidate.exists() and file_candidate.is_file(): + yamls.append(file_candidate) + + return yamls + + +def load_cli_config(codecov_yml_path: typing.Optional[pathlib.Path]): + if not codecov_yml_path: + yamls = _find_codecov_yamls() + codecov_yml_path = yamls[0] if yamls else None + + if not codecov_yml_path: + logger.warning("No config file could be found. Ignoring config.") + return None + + if not codecov_yml_path.exists() or not codecov_yml_path.is_file(): + logger.warning( + f"Config file {codecov_yml_path} not found, or is not a file. Ignoring config." + ) + return None -def load_cli_config(codecov_yml_path: pathlib.Path): - if codecov_yml_path.exists() and codecov_yml_path.is_file(): - logger.debug(f"Loading config from {codecov_yml_path}") - with open(codecov_yml_path, "r") as file_stream: - return yaml.safe_load(file_stream.read()) - logger.warning( - f"Config file {codecov_yml_path} not found, or is not a file. Ignoring config." - ) - return None + logger.debug(f"Loading config from {codecov_yml_path}") + with open(codecov_yml_path, "r") as file_stream: + return yaml.safe_load(file_stream.read()) diff --git a/codecov_cli/main.py b/codecov_cli/main.py index a1113a01..03e58d4f 100644 --- a/codecov_cli/main.py +++ b/codecov_cli/main.py @@ -34,8 +34,8 @@ ) @click.option( "--codecov-yml-path", - type=click.Path(path_type=pathlib.Path), - default=pathlib.Path("codecov.yml"), + type=click.Path(path_type=typing.Optional[pathlib.Path]), + default=None, ) @click.option( "--enterprise-url", "--url", "-u", help="Change the upload host (Enterprise use)" diff --git a/samples/fake_project/.codecov.yaml b/samples/fake_project/.codecov.yaml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/.codecov.yml b/samples/fake_project/.codecov.yml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/.github/.codecov.yaml b/samples/fake_project/.github/.codecov.yaml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/.github/.codecov.yml b/samples/fake_project/.github/.codecov.yml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/.github/codecov.yaml b/samples/fake_project/.github/codecov.yaml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/.github/codecov.yml b/samples/fake_project/.github/codecov.yml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/codecov.yaml b/samples/fake_project/codecov.yaml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/codecov.yml b/samples/fake_project/codecov.yml new file mode 100644 index 00000000..1ba7121f --- /dev/null +++ b/samples/fake_project/codecov.yml @@ -0,0 +1,5 @@ +runners: + python: + collect_tests_options: + - --ignore + - batata diff --git a/samples/fake_project/dev/.codecov.yaml b/samples/fake_project/dev/.codecov.yaml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/dev/.codecov.yml b/samples/fake_project/dev/.codecov.yml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/dev/codecov.yaml b/samples/fake_project/dev/codecov.yaml new file mode 100644 index 00000000..e69de29b diff --git a/samples/fake_project/dev/codecov.yml b/samples/fake_project/dev/codecov.yml new file mode 100644 index 00000000..e69de29b diff --git a/tests/helpers/test_config.py b/tests/helpers/test_config.py index 487f3975..4ec4f513 100644 --- a/tests/helpers/test_config.py +++ b/tests/helpers/test_config.py @@ -1,6 +1,7 @@ import pathlib +from unittest.mock import Mock -from codecov_cli.helpers.config import load_cli_config +from codecov_cli.helpers.config import _find_codecov_yamls, load_cli_config def test_load_config(mocker): @@ -21,3 +22,45 @@ def test_load_config_not_file(mocker): path = pathlib.Path("samples/") result = load_cli_config(path) assert result == None + + +def test_find_codecov_yaml(mocker): + fake_project_root = pathlib.Path.cwd() / "samples" / "fake_project" + + mock_vcs = Mock() + mock_vcs.get_network_root.return_value = fake_project_root + mocker.patch( + "codecov_cli.helpers.config.get_versioning_system", return_value=mock_vcs + ) + + expected_yamls = [ + fake_project_root / "codecov.yaml", + fake_project_root / ".codecov.yaml", + fake_project_root / "codecov.yml", + fake_project_root / ".codecov.yml", + fake_project_root / ".github" / "codecov.yaml", + fake_project_root / ".github" / ".codecov.yaml", + fake_project_root / ".github" / "codecov.yml", + fake_project_root / ".github" / ".codecov.yml", + fake_project_root / "dev" / "codecov.yaml", + fake_project_root / "dev" / ".codecov.yaml", + fake_project_root / "dev" / "codecov.yml", + fake_project_root / "dev" / ".codecov.yml", + ] + + assert sorted(_find_codecov_yamls()) == sorted(expected_yamls) + + +def test_load_config_finds_yaml(mocker): + fake_project_root = pathlib.Path.cwd() / "samples" / "fake_project" + + mock_vcs = Mock() + mock_vcs.get_network_root.return_value = fake_project_root + mocker.patch( + "codecov_cli.helpers.config.get_versioning_system", return_value=mock_vcs + ) + + result = load_cli_config(None) + assert result == { + "runners": {"python": {"collect_tests_options": ["--ignore", "batata"]}} + } From 7c5d8749dba5dcf2d5cd3273895e92258a077c27 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 1 Feb 2024 20:07:43 -0500 Subject: [PATCH 045/128] Prepare release 0.4.6 (#369) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 81b55ce7..33723a47 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.5", + version="0.4.6", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From da50de0d1dc9f33d01c616dcafd76a0619a0af3c Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:29:15 -0500 Subject: [PATCH 046/128] fix: use sys.exit() instead of just exit() (#371) Signed-off-by: joseph-sentry --- codecov_cli/helpers/request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index 51060604..d4898f61 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -1,4 +1,5 @@ import logging +from sys import exit from time import sleep import click From 5d309ec1694c7853a06ed5f8b04a63f8705a5880 Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:04:11 -0300 Subject: [PATCH 047/128] feat: make noop plugin recognizable (#373) * feat: make noop plugin recognizable context: https://github.com/codecov/feedback/issues/258 Currently there's no way to disable plugins. We don't want to change the default to preserve compatibility with the uploader little magic tricks that were executed by default (as far as I know). These changes allow you to pass the `--plugin noop` option to the `create-upload` command in order to not execute any plugin. * rename test function --- codecov_cli/plugins/__init__.py | 2 ++ tests/plugins/test_instantiation.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/codecov_cli/plugins/__init__.py b/codecov_cli/plugins/__init__.py index 0f900529..2ce8cb6d 100644 --- a/codecov_cli/plugins/__init__.py +++ b/codecov_cli/plugins/__init__.py @@ -60,6 +60,8 @@ def _load_plugin_from_yaml(plugin_dict: typing.Dict): def _get_plugin(cli_config, plugin_name): + if plugin_name == "noop": + return NoopPlugin() if plugin_name == "gcov": return GcovPlugin() if plugin_name == "pycoverage": diff --git a/tests/plugins/test_instantiation.py b/tests/plugins/test_instantiation.py index aa2a3a7d..caaa3516 100644 --- a/tests/plugins/test_instantiation.py +++ b/tests/plugins/test_instantiation.py @@ -115,6 +115,11 @@ def test_get_plugin_xcode(): assert isinstance(res, XcodePlugin) +def test_get_plugin_noop(): + res = _get_plugin({}, "noop") + assert isinstance(res, NoopPlugin) + + def test_get_plugin_pycoverage(): res = _get_plugin({}, "pycoverage") assert isinstance(res, Pycoverage) From 52642a08f9c1d24cce7b02170bfa238466d63827 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:01:32 -0500 Subject: [PATCH 048/128] build: upload test results (#363) Signed-off-by: joseph-sentry --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17ea81c3..fb55e118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,11 +76,12 @@ jobs: pip install -r tests/requirements.txt - name: Test with pytest run: | - pytest --cov + pytest --cov --junitxml=junit.xml - name: Dogfooding codecov-cli if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} run: | codecovcli do-upload --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} + codecovcli do-upload --report-type test_results --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} static-analysis: runs-on: ubuntu-latest From a89454580a06f6b9d6155a25bb90466782ec9493 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:32:55 -0500 Subject: [PATCH 049/128] fix: remove typing.Optional from codecov yaml path option (#374) Signed-off-by: joseph-sentry --- codecov_cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov_cli/main.py b/codecov_cli/main.py index 03e58d4f..1c364d36 100644 --- a/codecov_cli/main.py +++ b/codecov_cli/main.py @@ -34,7 +34,7 @@ ) @click.option( "--codecov-yml-path", - type=click.Path(path_type=typing.Optional[pathlib.Path]), + type=click.Path(path_type=pathlib.Path), default=None, ) @click.option( From 3662353398bba1ea5229f30ddf5ca44e510a09f0 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:16:54 -0500 Subject: [PATCH 050/128] fix: better default file pattern for test results (#379) Signed-off-by: joseph-sentry --- codecov_cli/services/upload/file_finder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index e98517de..57a5e515 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -36,7 +36,7 @@ ] test_results_files_patterns = [ - "*junit*", + "*junit.xml", ] coverage_files_excluded_patterns = [ From 618a641e82c9ab8597c16f112d951f230c435773 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 15 Feb 2024 15:14:44 -0500 Subject: [PATCH 051/128] Prepare release 0.4.7 (#381) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 33723a47..42072ec4 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.6", + version="0.4.7", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From c59058405c43a254cfc51933ea0747de1a1f602d Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:26:00 -0800 Subject: [PATCH 052/128] fix: update user file search (#390) --- codecov_cli/services/upload/file_finder.py | 13 +++--- .../upload/test_coverage_file_finder.py | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index 57a5e515..b2879fff 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -180,13 +180,13 @@ class FileFinder(object): def __init__( self, - project_root: Path = None, + search_root: Path = None, folders_to_ignore: typing.List[str] = None, explicitly_listed_files: typing.List[Path] = None, disable_search: bool = False, report_type: str = "coverage", ): - self.project_root = project_root or Path(os.getcwd()) + self.search_root = search_root or Path(os.getcwd()) self.folders_to_ignore = folders_to_ignore or [] self.explicitly_listed_files = explicitly_listed_files or None self.disable_search = disable_search @@ -207,7 +207,7 @@ def find_files(self) -> typing.List[UploadCollectionResultFile]: if not self.disable_search: regex_patterns_to_include = globs_to_regex(files_patterns) files_paths = search_files( - self.project_root, + self.search_root, default_folders_to_ignore + self.folders_to_ignore, filename_include_regex=regex_patterns_to_include, filename_exclude_regex=regex_patterns_to_exclude, @@ -243,16 +243,17 @@ def get_user_specified_files(self, regex_patterns_to_exclude): ) user_files_paths = list( search_files( - self.project_root, - default_folders_to_ignore + self.folders_to_ignore, + self.search_root, + self.folders_to_ignore, filename_include_regex=regex_patterns_to_include, filename_exclude_regex=regex_patterns_to_exclude, multipart_include_regex=multipart_include_regex, ) ) not_found_files = [] + user_files_paths_resolved = [path.resolve() for path in user_files_paths] for filepath in self.explicitly_listed_files: - if filepath.resolve() not in user_files_paths: + if filepath.resolve() not in user_files_paths_resolved: not_found_files.append(filepath) if not_found_files: diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index 953e6873..127fb31c 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -179,8 +179,10 @@ def test_find_coverage_files_with_existing_files(self): self.project_root / "coverage.xml", self.project_root / "subdirectory" / "test_coverage.xml", self.project_root / "other_file.txt", + self.project_root / ".tox" / "another_file.abc", ] (self.project_root / "subdirectory").mkdir() + (self.project_root / ".tox").mkdir() for file in coverage_files: file.touch() @@ -208,8 +210,10 @@ def test_find_coverage_files_with_disabled_search(self): self.project_root / "subdirectory" / "another_file.abc", self.project_root / "subdirectory" / "test_coverage.xml", self.project_root / "other_file.txt", + self.project_root / ".tox" / "another_file.abc", ] (self.project_root / "subdirectory").mkdir() + (self.project_root / ".tox").mkdir() for file in coverage_files: file.touch() @@ -237,8 +241,10 @@ def test_find_coverage_files_with_user_specified_files(self): self.project_root / "subdirectory" / "test_coverage.xml", self.project_root / "test_file.abc", self.project_root / "subdirectory" / "another_file.abc", + self.project_root / ".tox" / "another_file.abc", ] (self.project_root / "subdirectory").mkdir() + (self.project_root / ".tox").mkdir() for file in coverage_files: file.touch() @@ -264,8 +270,10 @@ def test_find_coverage_files_with_user_specified_files_not_found(self): coverage_files = [ self.project_root / "coverage.xml", self.project_root / "subdirectory" / "test_coverage.xml", + self.project_root / ".tox" / "another_file.abc", ] (self.project_root / "subdirectory").mkdir() + (self.project_root / ".tox").mkdir() for file in coverage_files: file.touch() @@ -286,3 +294,36 @@ def test_find_coverage_files_with_user_specified_files_not_found(self): ] expected_paths = sorted([file.get_filename() for file in expected]) self.assertEqual(result, expected_paths) + + def test_find_coverage_files_with_user_specified_files_in_default_ignored_folder(self): + # Create some sample coverage files + coverage_files = [ + self.project_root / "coverage.xml", + self.project_root / "subdirectory" / "test_coverage.xml", + self.project_root / "test_file.abc", + self.project_root / "subdirectory" / "another_file.abc", + self.project_root / ".tox" / "another_file.abc", + ] + (self.project_root / "subdirectory").mkdir() + (self.project_root / ".tox").mkdir() + for file in coverage_files: + file.touch() + + self.coverage_file_finder.explicitly_listed_files = [ + self.project_root / ".tox" / "another_file.abc", + ] + result = sorted( + [file.get_filename() for file in self.coverage_file_finder.find_files()] + ) + + expected = [ + UploadCollectionResultFile(Path(f"{self.project_root}/coverage.xml")), + UploadCollectionResultFile( + Path(f"{self.project_root}/subdirectory/test_coverage.xml") + ), + UploadCollectionResultFile( + Path(f"{self.project_root}/.tox/another_file.abc") + ), + ] + expected_paths = sorted([file.get_filename() for file in expected]) + self.assertEqual(result, expected_paths) From 5a0b46ab91091479ab8e47d8388e58c27cbf92f3 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Tue, 27 Feb 2024 12:54:25 -0500 Subject: [PATCH 053/128] Prepare release 0.4.8 (#391) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42072ec4..6d89dcdb 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.7", + version="0.4.8", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 92d7e890e61ea654d69cda86e198bf67db94a072 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:41:04 -0500 Subject: [PATCH 054/128] chore: fix typo in help description (#394) Signed-off-by: joseph-sentry --- codecov_cli/commands/upload.py | 2 +- tests/commands/test_invoke_upload_process.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index fafb34af..77c52a19 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -63,7 +63,7 @@ def _turn_env_vars_into_dict(ctx, params, value): ), click.option( "--disable-search", - help="Disable search for coverage files. This is helpful when specifying what files you want to uload with the --file option.", + help="Disable search for coverage files. This is helpful when specifying what files you want to upload with the --file option.", is_flag=True, default=False, ), diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 67e88607..cac7a116 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -91,7 +91,7 @@ def test_upload_process_options(mocker): " disable uploading other files.", " --disable-search Disable search for coverage files. This is", " helpful when specifying what files you want to", - " uload with the --file option.", + " upload with the --file option.", " --disable-file-fixes Disable file fixes to ignore common lines from", " coverage (e.g. blank lines or empty brackets)", " -b, --build, --build-code TEXT Specify the build number manually", From ff1c300f8823444a1f2ed3ec4a9415d1af82988b Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:15:50 +0100 Subject: [PATCH 055/128] fix: better detect merge-commits (#397) Apparently a common thing for CIs to do is create a merge-commit for changes in a branch before running tests and stuff. This means that - especially for not-directly-supported CIs - we would maybe return a SHA for a commit that didn't exist in the branch. These changes fix that by checking to see if the current commit is a merge commit. If it is we return the second parent, the most recent commit before the current one. Q: What if the current latest commit in the branch is a merge commit? Well in this case the parent, which is also part of the branch, will have the coverage. Users can still provide a commit sha value to override this behavior. closes codecov/codecov-cli#372 --- codecov_cli/helpers/versioning_systems.py | 18 +++++++++++++-- tests/helpers/test_versioning_systems.py | 23 ++++++++++++++----- .../upload/test_coverage_file_finder.py | 4 +++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/codecov_cli/helpers/versioning_systems.py b/codecov_cli/helpers/versioning_systems.py index ae4bdc72..df0124e8 100644 --- a/codecov_cli/helpers/versioning_systems.py +++ b/codecov_cli/helpers/versioning_systems.py @@ -42,6 +42,20 @@ def is_available(cls): def get_fallback_value(self, fallback_field: FallbackFieldEnum): if fallback_field == FallbackFieldEnum.commit_sha: + # here we will get the commit SHA of the latest commit + # that is NOT a merge commit + p = subprocess.run( + # List current commit parent's SHA + ["git", "rev-parse", "HEAD^@"], + capture_output=True, + ) + parents_hash = p.stdout.decode().strip().splitlines() + if len(parents_hash) == 2: + # IFF the current commit is a merge commit it will have 2 parents + # We return the 2nd one - The commit that came from the branch merged into ours + return parents_hash[1] + # At this point we know the current commit is not a merge commit + # so we get it's SHA and return that p = subprocess.run(["git", "log", "-1", "--format=%H"], capture_output=True) if p.stdout: return p.stdout.decode().strip() @@ -56,7 +70,7 @@ def get_fallback_value(self, fallback_field: FallbackFieldEnum): return branch_name if branch_name != "HEAD" else None if fallback_field == FallbackFieldEnum.slug: - # if there are multiple remotes, we will prioritize using the one called 'origin' if it exsits, else we will use the first one in 'git remote' list + # if there are multiple remotes, we will prioritize using the one called 'origin' if it exists, else we will use the first one in 'git remote' list p = subprocess.run(["git", "remote"], capture_output=True) @@ -78,7 +92,7 @@ def get_fallback_value(self, fallback_field: FallbackFieldEnum): return parse_slug(remote_url) if fallback_field == FallbackFieldEnum.git_service: - # if there are multiple remotes, we will prioritize using the one called 'origin' if it exsits, else we will use the first one in 'git remote' list + # if there are multiple remotes, we will prioritize using the one called 'origin' if it exists, else we will use the first one in 'git remote' list p = subprocess.run(["git", "remote"], capture_output=True) if not p.stdout: diff --git a/tests/helpers/test_versioning_systems.py b/tests/helpers/test_versioning_systems.py index c0532ef6..cc74410f 100644 --- a/tests/helpers/test_versioning_systems.py +++ b/tests/helpers/test_versioning_systems.py @@ -8,17 +8,28 @@ class TestGitVersioningSystem(object): @pytest.mark.parametrize( - "commit_sha,expected", [("", None), (b" random_sha ", "random_sha")] + "runs_output,expected", + [ + # No output for parents nor commit + ([b"", b""], None), + # No output for parents, commit has SHA + ([b"", b" random_sha"], "random_sha"), + # Commit is NOT a merge-commit + ([b" parent_sha", b" random_sha "], "random_sha"), + # Commit IS a merge-commit + ([b" parent_sha0\nparent_sha1", b" random_sha"], "parent_sha1"), + ], ) - def test_commit_sha(self, mocker, commit_sha, expected): - mocked_subprocess = MagicMock() + def test_commit_sha(self, mocker, runs_output, expected): + mocked_subprocess = [ + MagicMock(**{"stdout": runs_output[0]}), + MagicMock(**{"stdout": runs_output[1]}), + ] mocker.patch( "codecov_cli.helpers.versioning_systems.subprocess.run", - return_value=mocked_subprocess, + side_effect=mocked_subprocess, ) - mocked_subprocess.stdout = commit_sha - assert ( GitVersioningSystem().get_fallback_value(FallbackFieldEnum.commit_sha) == expected diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index 127fb31c..09c09c80 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -295,7 +295,9 @@ def test_find_coverage_files_with_user_specified_files_not_found(self): expected_paths = sorted([file.get_filename() for file in expected]) self.assertEqual(result, expected_paths) - def test_find_coverage_files_with_user_specified_files_in_default_ignored_folder(self): + def test_find_coverage_files_with_user_specified_files_in_default_ignored_folder( + self, + ): # Create some sample coverage files coverage_files = [ self.project_root / "coverage.xml", From c198e38c9e57cbdd82bb12bad6d54b98e6d6c134 Mon Sep 17 00:00:00 2001 From: Jamie Macey Date: Thu, 7 Mar 2024 14:08:47 -0800 Subject: [PATCH 056/128] Fix compatibility on non-git project folders when git is installed on the system (#399) * fix readme typo * Only use GitVersioningSystem if we can find a .git folder --- README.md | 2 +- codecov_cli/helpers/versioning_systems.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f55cef33..1a09683b 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Codecov-cli supports user input. These inputs, along with their descriptions and | `get-report-results` | Used for local upload. It asks codecov to provide you the report results you calculated with the previous command. | `pr-base-picking` | Tells codecov that you want to explicitly define a base for your PR | `upload-process` | A wrapper for 3 commands. Create-commit, create-report and do-upload. You can use this command to upload to codecov instead of using the previosly mentioned commands. -| `send-notification` | A command that tells Codecov that you finished uploading and you want to be sent notifications. To disable automatically sent notifications please consider adding manual_trigger to your codecov.yml, so it will look like codecov: notify: manual_trigger: true. +| `send-notifications` | A command that tells Codecov that you finished uploading and you want to be sent notifications. To disable automatically sent notifications please consider adding manual_trigger to your codecov.yml, so it will look like codecov: notify: manual_trigger: true. >**Note**: Every command has its own different options that will be mentioned later in this doc. Codecov will try to load these options from your CI environment variables, if not, it will try to load them from git, if not found, you may need to add them manually. diff --git a/codecov_cli/helpers/versioning_systems.py b/codecov_cli/helpers/versioning_systems.py index df0124e8..07763876 100644 --- a/codecov_cli/helpers/versioning_systems.py +++ b/codecov_cli/helpers/versioning_systems.py @@ -38,7 +38,13 @@ def get_versioning_system() -> VersioningSystemInterface: class GitVersioningSystem(VersioningSystemInterface): @classmethod def is_available(cls): - return which("git") is not None + if which("git") is not None: + p = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], capture_output=True + ) + if p.stdout: + return True + return False def get_fallback_value(self, fallback_field: FallbackFieldEnum): if fallback_field == FallbackFieldEnum.commit_sha: From ced50947b69a73514be3e420a2ba36917530370e Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:27:51 -0500 Subject: [PATCH 057/128] feat: retry requests on 502 responses (#382) --- codecov_cli/helpers/request.py | 14 +++++++++++++- tests/helpers/test_upload_sender.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index d4898f61..d356eed8 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -47,15 +47,27 @@ def backoff_time(curr_retry): return 2 ** (curr_retry - 1) +class RetryException(Exception): + ... + + def retry_request(func): def wrapper(*args, **kwargs): retry = 0 while retry < MAX_RETRIES: try: - return func(*args, **kwargs) + response = func(*args, **kwargs) + if response.status_code == 502: + logger.warning( + "Response status code was 502.", + extra=dict(extra_log_attributes=dict(retry=retry)), + ) + raise RetryException + return response except ( requests.exceptions.ConnectionError, requests.exceptions.Timeout, + RetryException, ) as exp: logger.warning( "Request failed. Retrying", diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 1d46dbfa..3bfb97ea 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -1,4 +1,5 @@ import json +import re from pathlib import Path import pytest @@ -304,6 +305,23 @@ def test_upload_sender_result_fail_post_400( assert sender.warnings is not None + def test_upload_sender_result_fail_post_502( + self, mocker, mocked_responses, mocked_legacy_upload_endpoint, capsys + ): + mocker.patch("codecov_cli.helpers.request.sleep") + mocked_legacy_upload_endpoint.status = 502 + + with pytest.raises(Exception, match="Request failed after too many retries"): + _ = UploadSender().send_upload_data( + upload_collection, random_sha, random_token, **named_upload_data + ) + + matcher = re.compile( + r"(warning.*((Response status code was 502)|(Request failed\. Retrying)).*(\n)?){6}" + ) + + assert matcher.match(capsys.readouterr().err) is not None + def test_upload_sender_result_fail_put_400( self, mocked_responses, mocked_legacy_upload_endpoint, mocked_storage_server ): From ccf861226571ab4fc694013093689deed45e7d80 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:57:56 -0500 Subject: [PATCH 058/128] chore: improve error message for finding reports (#396) also runs `make lint` --- codecov_cli/services/upload/upload_collector.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 282c0e99..d687ab98 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -153,9 +153,13 @@ def generate_upload_data(self, report_type="coverage") -> UploadCollectionResult files = self.file_finder.find_files() logger.info(f"Found {len(files)} {report_type} files to upload") if not files: + if report_type == "test_results": + error_message = "No JUnit XML reports found. Please review our documentation (https://docs.codecov.com/docs/test-result-ingestion-beta) to generate and upload the file." + else: + error_message = "No coverage reports found. Please make sure you're generating reports successfully." raise click.ClickException( click.style( - f"No {report_type} reports found. Please make sure you're generating reports successfully.", + error_message, fg="red", ) ) @@ -164,7 +168,9 @@ def generate_upload_data(self, report_type="coverage") -> UploadCollectionResult return UploadCollectionResult( network=network, files=files, - file_fixes=self._produce_file_fixes_for_network(network) - if report_type == "coverage" - else [], + file_fixes=( + self._produce_file_fixes_for_network(network) + if report_type == "coverage" + else [] + ), ) From 22725470a116d0426995b8f84352c25342a6f02c Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Mon, 11 Mar 2024 09:50:31 -0400 Subject: [PATCH 059/128] fix: add ci_service to upload data (#398) * fix: add ci_service to upload data * test: fix tests for ci_service Signed-off-by: joseph-sentry --- codecov_cli/services/upload/upload_sender.py | 1 + tests/helpers/test_upload_sender.py | 1 + 2 files changed, 2 insertions(+) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index f1399d48..3283f8e9 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -51,6 +51,7 @@ def send_upload_data( "name": name, "job_code": job_code, "version": codecov_cli_version, + "ci_service": ci_service, } # Data to upload to Codecov diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 3bfb97ea..054974fc 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -52,6 +52,7 @@ "job_code": "job_code", "name": "name", "version": codecov_cli_version, + "ci_service": "ci_service", } From 5a36952dc84b9d9e4e62599cf2bb6f9abd93e48d Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:45:18 -0400 Subject: [PATCH 060/128] feat: include user specified files even if excluded (#383) * feat: include user specified files even if excluded * fix: remove unnecessary changes to file finder * fix: add back unnecessarily removed test Signed-off-by: joseph-sentry --- codecov_cli/services/upload/file_finder.py | 3 +- .../upload/test_coverage_file_finder.py | 280 ++++++++++++------ 2 files changed, 185 insertions(+), 98 deletions(-) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index b2879fff..a1fb3be2 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -232,7 +232,7 @@ def get_user_specified_files(self, regex_patterns_to_exclude): files_excluded_but_user_includes.append(str(file)) if files_excluded_but_user_includes: logger.warning( - "Some files being explicitly added are found in the list of excluded files for upload.", + "Some files being explicitly added are found in the list of excluded files for upload. We are still going to search for the explicitly added files.", extra=dict( extra_log_attributes=dict(files=files_excluded_but_user_includes) ), @@ -246,7 +246,6 @@ def get_user_specified_files(self, regex_patterns_to_exclude): self.search_root, self.folders_to_ignore, filename_include_regex=regex_patterns_to_include, - filename_exclude_regex=regex_patterns_to_exclude, multipart_include_regex=multipart_include_regex, ) ) diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index 09c09c80..6b67cd38 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -1,7 +1,8 @@ import tempfile -import unittest from pathlib import Path +import pytest + from codecov_cli.services.upload.file_finder import FileFinder from codecov_cli.types import UploadCollectionResultFile @@ -153,179 +154,266 @@ def test_find_coverage_files_test_results(self, tmp_path): assert actual - expected == {UploadCollectionResultFile(extra)} -class TestCoverageFileFinderUserInput(unittest.TestCase): - def setUp(self): - self.temp_dir = tempfile.TemporaryDirectory() # Create a temporary directory - self.project_root = Path(self.temp_dir.name) - self.folders_to_ignore = [] - self.explicitly_listed_files = [ - self.project_root / "test_file.abc", - self.project_root / "subdirectory" / "another_file.abc", - ] - self.disable_search = False - self.coverage_file_finder = FileFinder( - self.project_root, - self.folders_to_ignore, - self.explicitly_listed_files, - self.disable_search, - ) - - def tearDown(self): - self.temp_dir.cleanup() # Clean up the temporary directory - - def test_find_coverage_files_with_existing_files(self): - # Create some sample coverage files +@pytest.fixture() +def coverage_file_finder_fixture(): + temp_dir = tempfile.TemporaryDirectory() # Create a temporary directory + project_root = Path(temp_dir.name) + folders_to_ignore = [] + explicitly_listed_files = [ + project_root / "test_file.abc", + project_root / "subdirectory" / "another_file.abc", + ] + disable_search = False + coverage_file_finder = FileFinder( + project_root, + folders_to_ignore, + explicitly_listed_files, + disable_search, + ) + yield project_root, coverage_file_finder + temp_dir.cleanup() + + +class TestCoverageFileFinderUserInput: + def test_find_coverage_files_with_existing_files( + self, coverage_file_finder_fixture + ): + # Create some sample coverage coverage_file_finder_fixture + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture coverage_files = [ - self.project_root / "coverage.xml", - self.project_root / "subdirectory" / "test_coverage.xml", - self.project_root / "other_file.txt", - self.project_root / ".tox" / "another_file.abc", + project_root / "coverage.xml", + project_root / "subdirectory" / "test_coverage.xml", + project_root / "other_file.txt", + project_root / ".tox" / "another_file.abc", ] - (self.project_root / "subdirectory").mkdir() - (self.project_root / ".tox").mkdir() + (project_root / "subdirectory").mkdir() + (project_root / ".tox").mkdir() for file in coverage_files: file.touch() result = sorted( - [file.get_filename() for file in self.coverage_file_finder.find_files()] + [file.get_filename() for file in coverage_file_finder.find_files()] ) expected = [ - UploadCollectionResultFile(Path(f"{self.project_root}/coverage.xml")), + UploadCollectionResultFile(Path(f"{project_root}/coverage.xml")), UploadCollectionResultFile( - Path(f"{self.project_root}/subdirectory/test_coverage.xml") + Path(f"{project_root}/subdirectory/test_coverage.xml") ), ] expected_paths = sorted([file.get_filename() for file in expected]) - self.assertEqual(result, expected_paths) - - def test_find_coverage_files_with_no_files(self): - result = self.coverage_file_finder.find_files() - self.assertEqual(result, []) - - def test_find_coverage_files_with_disabled_search(self): - # Create some sample coverage files - print("project root", self.project_root) + assert result == expected_paths + + def test_find_coverage_files_with_no_files(self, coverage_file_finder_fixture): + ( + _, + coverage_file_finder, + ) = coverage_file_finder_fixture + result = coverage_file_finder.find_files() + assert result == [] + + def test_find_coverage_files_with_disabled_search( + self, coverage_file_finder_fixture + ): + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture + # Create some sample coverage coverage_file_finder_fixture + print("project root", project_root) coverage_files = [ - self.project_root / "test_file.abc", - self.project_root / "subdirectory" / "another_file.abc", - self.project_root / "subdirectory" / "test_coverage.xml", - self.project_root / "other_file.txt", - self.project_root / ".tox" / "another_file.abc", + project_root / "test_file.abc", + project_root / "subdirectory" / "another_file.abc", + project_root / "subdirectory" / "test_coverage.xml", + project_root / "other_file.txt", + project_root / ".tox" / "another_file.abc", ] - (self.project_root / "subdirectory").mkdir() - (self.project_root / ".tox").mkdir() + (project_root / "subdirectory").mkdir() + (project_root / ".tox").mkdir() for file in coverage_files: file.touch() # Disable search - self.coverage_file_finder.disable_search = True + coverage_file_finder.disable_search = True result = sorted( - [file.get_filename() for file in self.coverage_file_finder.find_files()] + [file.get_filename() for file in coverage_file_finder.find_files()] ) expected = [ - UploadCollectionResultFile(Path(f"{self.project_root}/test_file.abc")), + UploadCollectionResultFile(Path(f"{project_root}/test_file.abc")), UploadCollectionResultFile( - Path(f"{self.project_root}/subdirectory/another_file.abc") + Path(f"{project_root}/subdirectory/another_file.abc") ), ] expected_paths = sorted([file.get_filename() for file in expected]) - self.assertEqual(result, expected_paths) + assert result == expected_paths - def test_find_coverage_files_with_user_specified_files(self): - # Create some sample coverage files + def test_find_coverage_files_with_user_specified_files( + self, coverage_file_finder_fixture + ): + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture + + # Create some sample coverage coverage_file_finder_fixture coverage_files = [ - self.project_root / "coverage.xml", - self.project_root / "subdirectory" / "test_coverage.xml", - self.project_root / "test_file.abc", - self.project_root / "subdirectory" / "another_file.abc", - self.project_root / ".tox" / "another_file.abc", + project_root / "coverage.xml", + project_root / "subdirectory" / "test_coverage.xml", + project_root / "test_file.abc", + project_root / "subdirectory" / "another_file.abc", + project_root / ".tox" / "another_file.abc", ] - (self.project_root / "subdirectory").mkdir() - (self.project_root / ".tox").mkdir() + (project_root / "subdirectory").mkdir() + (project_root / ".tox").mkdir() for file in coverage_files: file.touch() result = sorted( - [file.get_filename() for file in self.coverage_file_finder.find_files()] + [file.get_filename() for file in coverage_file_finder.find_files()] ) expected = [ - UploadCollectionResultFile(Path(f"{self.project_root}/coverage.xml")), + UploadCollectionResultFile(Path(f"{project_root}/coverage.xml")), UploadCollectionResultFile( - Path(f"{self.project_root}/subdirectory/test_coverage.xml") + Path(f"{project_root}/subdirectory/test_coverage.xml") ), - UploadCollectionResultFile(Path(f"{self.project_root}/test_file.abc")), + UploadCollectionResultFile(Path(f"{project_root}/test_file.abc")), UploadCollectionResultFile( - Path(f"{self.project_root}/subdirectory/another_file.abc") + Path(f"{project_root}/subdirectory/another_file.abc") ), ] expected_paths = sorted([file.get_filename() for file in expected]) - self.assertEqual(result, expected_paths) + assert result == expected_paths - def test_find_coverage_files_with_user_specified_files_not_found(self): - # Create some sample coverage files + def test_find_coverage_files_with_user_specified_files_not_found( + self, coverage_file_finder_fixture + ): + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture + + # Create some sample coverage coverage_file_finder_fixture coverage_files = [ - self.project_root / "coverage.xml", - self.project_root / "subdirectory" / "test_coverage.xml", - self.project_root / ".tox" / "another_file.abc", + project_root / "coverage.xml", + project_root / "subdirectory" / "test_coverage.xml", + project_root / ".tox" / "another_file.abc", ] - (self.project_root / "subdirectory").mkdir() - (self.project_root / ".tox").mkdir() + (project_root / "subdirectory").mkdir() + (project_root / ".tox").mkdir() for file in coverage_files: file.touch() # Add a non-existent file to explicitly_listed_files - self.coverage_file_finder.explicitly_listed_files.append( - self.project_root / "non_existent.xml" + coverage_file_finder.explicitly_listed_files.append( + project_root / "non_existent.xml" ) result = sorted( - [file.get_filename() for file in self.coverage_file_finder.find_files()] + [file.get_filename() for file in coverage_file_finder.find_files()] ) expected = [ - UploadCollectionResultFile(Path(f"{self.project_root}/coverage.xml")), + UploadCollectionResultFile(Path(f"{project_root}/coverage.xml")), UploadCollectionResultFile( - Path(f"{self.project_root}/subdirectory/test_coverage.xml") + Path(f"{project_root}/subdirectory/test_coverage.xml") ), ] expected_paths = sorted([file.get_filename() for file in expected]) - self.assertEqual(result, expected_paths) + assert result == expected_paths def test_find_coverage_files_with_user_specified_files_in_default_ignored_folder( - self, + self, coverage_file_finder_fixture ): + + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture + # Create some sample coverage files coverage_files = [ - self.project_root / "coverage.xml", - self.project_root / "subdirectory" / "test_coverage.xml", - self.project_root / "test_file.abc", - self.project_root / "subdirectory" / "another_file.abc", - self.project_root / ".tox" / "another_file.abc", + project_root / "coverage.xml", + project_root / "subdirectory" / "test_coverage.xml", + project_root / "test_file.abc", + project_root / "subdirectory" / "another_file.abc", + project_root / ".tox" / "another_file.abc", ] - (self.project_root / "subdirectory").mkdir() - (self.project_root / ".tox").mkdir() + (project_root / "subdirectory").mkdir() + (project_root / ".tox").mkdir() for file in coverage_files: file.touch() - self.coverage_file_finder.explicitly_listed_files = [ - self.project_root / ".tox" / "another_file.abc", + coverage_file_finder.explicitly_listed_files = [ + project_root / ".tox" / "another_file.abc", + ] + result = sorted( + [file.get_filename() for file in coverage_file_finder.find_files()] + ) + + expected = [ + UploadCollectionResultFile(Path(f"{project_root}/coverage.xml")), + UploadCollectionResultFile( + Path(f"{project_root}/subdirectory/test_coverage.xml") + ), + UploadCollectionResultFile(Path(f"{project_root}/.tox/another_file.abc")), ] + expected_paths = sorted([file.get_filename() for file in expected]) + + assert result == expected_paths + + def test_find_coverage_files_with_user_specified_files_in_excluded( + self, capsys, coverage_file_finder_fixture + ): + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture + + # Create some sample coverage coverage_file_finder_fixture + coverage_files = [ + project_root / "coverage.xml", + project_root / "subdirectory" / "test_coverage.xml", + project_root / "test_file.abc", + project_root / "subdirectory" / "another_file.abc", + project_root / "subdirectory" / "another_file.bash", + project_root / ".tox" / "another_file.abc", + ] + (project_root / "subdirectory").mkdir() + (project_root / ".tox").mkdir() + for file in coverage_files: + file.touch() + + coverage_file_finder.explicitly_listed_files.append( + project_root / "subdirectory" / "another_file.bash" + ) result = sorted( - [file.get_filename() for file in self.coverage_file_finder.find_files()] + [file.get_filename() for file in coverage_file_finder.find_files()] ) expected = [ - UploadCollectionResultFile(Path(f"{self.project_root}/coverage.xml")), + UploadCollectionResultFile(Path(f"{project_root}/coverage.xml")), UploadCollectionResultFile( - Path(f"{self.project_root}/subdirectory/test_coverage.xml") + Path(f"{project_root}/subdirectory/test_coverage.xml") ), + UploadCollectionResultFile(Path(f"{project_root}/test_file.abc")), UploadCollectionResultFile( - Path(f"{self.project_root}/.tox/another_file.abc") + Path(f"{project_root}/subdirectory/another_file.abc") + ), + UploadCollectionResultFile( + Path(f"{project_root}/subdirectory/another_file.bash") ), ] expected_paths = sorted([file.get_filename() for file in expected]) - self.assertEqual(result, expected_paths) + + assert result == expected_paths + + assert ( + "Some files being explicitly added are found in the list of excluded files for upload. We are still going to search for the explicitly added files." + in capsys.readouterr().err + ) From 3ce68bd1de511f65cbb44189fd08188c8574b67d Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 27 Mar 2024 11:09:24 -0400 Subject: [PATCH 061/128] Prepare release 0.4.9 (#402) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6d89dcdb..4c453dff 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.8", + version="0.4.9", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 750e9578228be7fd4ab32282847f9dd0900630bf Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:14:48 -0700 Subject: [PATCH 062/128] feat: add network prefix and filter options (#406) * feat: add network prefix and filter options * fix: update network tests * fix: add tests * fix: pull all files for file fixes * fix: added description to network-filter --- codecov_cli/commands/upload.py | 106 ++++++++++-------- codecov_cli/commands/upload_process.py | 2 + codecov_cli/services/upload/__init__.py | 41 ++++--- codecov_cli/services/upload/network_finder.py | 42 +++++-- .../services/upload/upload_collector.py | 20 ++-- tests/commands/test_invoke_upload_process.py | 8 ++ tests/helpers/test_network_finder.py | 41 ++++++- .../services/upload/test_upload_collector.py | 14 +-- tests/services/upload/test_upload_service.py | 28 +++-- 9 files changed, 202 insertions(+), 100 deletions(-) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index 77c52a19..aa9e7f75 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -164,6 +164,14 @@ def _turn_env_vars_into_dict(ctx, params, value): default="coverage", type=click.Choice(["coverage", "test_results"]), ), + click.option( + "--network-filter", + help="Specify a filter on the files listed in the network section of the Codecov report. This will only add files whose path begin with the specified filter. Useful for upload-specific path fixing", + ), + click.option( + "--network-prefix", + help="Specify a prefix on files listed in the network section of the Codecov report. Useful to help resolve path fixing", + ), ] @@ -181,29 +189,31 @@ def do_upload( ctx: click.Context, commit_sha: str, report_code: str, + branch: typing.Optional[str], build_code: typing.Optional[str], build_url: typing.Optional[str], - job_code: typing.Optional[str], + disable_file_fixes: bool, + disable_search: bool, + dry_run: bool, env_vars: typing.Dict[str, str], + fail_on_error: bool, + files_search_exclude_folders: typing.List[pathlib.Path], + files_search_explicitly_listed_files: typing.List[pathlib.Path], + files_search_root_folder: pathlib.Path, flags: typing.List[str], + git_service: typing.Optional[str], + handle_no_reports_found: bool, + job_code: typing.Optional[str], name: typing.Optional[str], + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], network_root_folder: pathlib.Path, - files_search_root_folder: pathlib.Path, - files_search_exclude_folders: typing.List[pathlib.Path], - files_search_explicitly_listed_files: typing.List[pathlib.Path], - disable_search: bool, - disable_file_fixes: bool, - token: typing.Optional[str], plugin_names: typing.List[str], - branch: typing.Optional[str], - slug: typing.Optional[str], pull_request_number: typing.Optional[str], - use_legacy_uploader: bool, - fail_on_error: bool, - dry_run: bool, - git_service: typing.Optional[str], - handle_no_reports_found: bool, report_type: str, + slug: typing.Optional[str], + token: typing.Optional[str], + use_legacy_uploader: bool, ): versioning_system = ctx.obj["versioning_system"] codecov_yaml = ctx.obj["codecov_yaml"] or {} @@ -214,29 +224,31 @@ def do_upload( "Starting upload processing", extra=dict( extra_log_attributes=dict( - upload_file_type=report_type, - commit_sha=commit_sha, - report_code=report_code, + branch=branch, build_code=build_code, build_url=build_url, - job_code=job_code, + commit_sha=commit_sha, + disable_file_fixes=disable_file_fixes, + disable_search=disable_search, + enterprise_url=enterprise_url, env_vars=env_vars, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, flags=flags, + git_service=git_service, + handle_no_reports_found=handle_no_reports_found, + job_code=job_code, name=name, + network_filter=network_filter, + network_prefix=network_prefix, network_root_folder=network_root_folder, - files_search_root_folder=files_search_root_folder, - files_search_exclude_folders=files_search_exclude_folders, - files_search_explicitly_listed_files=files_search_explicitly_listed_files, plugin_names=plugin_names, - token=token, - branch=branch, - slug=slug, pull_request_number=pull_request_number, - git_service=git_service, - enterprise_url=enterprise_url, - disable_search=disable_search, - disable_file_fixes=disable_file_fixes, - handle_no_reports_found=handle_no_reports_found, + report_code=report_code, + slug=slug, + token=token, + upload_file_type=report_type, ) ), ) @@ -244,30 +256,32 @@ def do_upload( cli_config, versioning_system, ci_adapter, - upload_file_type=report_type, - commit_sha=commit_sha, - report_code=report_code, + branch=branch, build_code=build_code, build_url=build_url, - job_code=job_code, + commit_sha=commit_sha, + disable_file_fixes=disable_file_fixes, + disable_search=disable_search, + dry_run=dry_run, + enterprise_url=enterprise_url, env_vars=env_vars, + fail_on_error=fail_on_error, + files_search_exclude_folders=list(files_search_exclude_folders), + files_search_explicitly_listed_files=list(files_search_explicitly_listed_files), + files_search_root_folder=files_search_root_folder, flags=flags, + git_service=git_service, + handle_no_reports_found=handle_no_reports_found, + job_code=job_code, name=name, + network_filter=network_filter, + network_prefix=network_prefix, network_root_folder=network_root_folder, - files_search_root_folder=files_search_root_folder, - files_search_exclude_folders=list(files_search_exclude_folders), - files_search_explicitly_listed_files=list(files_search_explicitly_listed_files), plugin_names=plugin_names, - token=token, - branch=branch, - slug=slug, pull_request_number=pull_request_number, + report_code=report_code, + slug=slug, + token=token, + upload_file_type=report_type, use_legacy_uploader=use_legacy_uploader, - fail_on_error=fail_on_error, - dry_run=dry_run, - git_service=git_service, - enterprise_url=enterprise_url, - disable_search=disable_search, - handle_no_reports_found=handle_no_reports_found, - disable_file_fixes=disable_file_fixes, ) diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index 20b67352..2f5acd3e 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -31,6 +31,8 @@ def upload_process( env_vars: typing.Dict[str, str], flags: typing.List[str], name: typing.Optional[str], + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], network_root_folder: pathlib.Path, files_search_root_folder: pathlib.Path, files_search_exclude_folders: typing.List[pathlib.Path], diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index 30960ae6..a8fc64d7 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -25,32 +25,34 @@ def do_upload_logic( versioning_system: VersioningSystemInterface, ci_adapter: CIAdapterBase, *, - commit_sha: str, - report_code: str, + branch: typing.Optional[str], build_code: typing.Optional[str], build_url: typing.Optional[str], - job_code: typing.Optional[str], + commit_sha: str, + disable_file_fixes: bool = False, + disable_search: bool = False, + dry_run: bool = False, + enterprise_url: typing.Optional[str], env_vars: typing.Dict[str, str], + fail_on_error: bool = False, + files_search_exclude_folders: typing.List[Path], + files_search_explicitly_listed_files: typing.List[Path], + files_search_root_folder: Path, flags: typing.List[str], + git_service: typing.Optional[str], + handle_no_reports_found: bool = False, + job_code: typing.Optional[str], name: typing.Optional[str], + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], network_root_folder: Path, - files_search_root_folder: Path, - files_search_exclude_folders: typing.List[Path], - files_search_explicitly_listed_files: typing.List[Path], plugin_names: typing.List[str], - token: str, - branch: typing.Optional[str], - slug: typing.Optional[str], pull_request_number: typing.Optional[str], + report_code: str, + slug: typing.Optional[str], + token: str, upload_file_type: str = "coverage", use_legacy_uploader: bool = False, - fail_on_error: bool = False, - dry_run: bool = False, - git_service: typing.Optional[str], - enterprise_url: typing.Optional[str], - disable_search: bool = False, - handle_no_reports_found: bool = False, - disable_file_fixes: bool = False, ): if upload_file_type == "coverage": preparation_plugins = select_preparation_plugins(cli_config, plugin_names) @@ -63,7 +65,12 @@ def do_upload_logic( disable_search, upload_file_type, ) - network_finder = select_network_finder(versioning_system) + network_finder = select_network_finder( + versioning_system, + network_filter=network_filter, + network_prefix=network_prefix, + network_root_folder=network_root_folder, + ) collector = UploadCollector( preparation_plugins, network_finder, file_selector, disable_file_fixes ) diff --git a/codecov_cli/services/upload/network_finder.py b/codecov_cli/services/upload/network_finder.py index 3ccfb463..8da568eb 100644 --- a/codecov_cli/services/upload/network_finder.py +++ b/codecov_cli/services/upload/network_finder.py @@ -5,17 +5,39 @@ class NetworkFinder(object): - def __init__(self, versioning_system: VersioningSystemInterface): + def __init__( + self, + versioning_system: VersioningSystemInterface, + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], + network_root_folder: pathlib.Path, + ): self.versioning_system = versioning_system + self.network_filter = network_filter + self.network_prefix = network_prefix + self.network_root_folder = network_root_folder - def find_files( - self, - network_root: typing.Optional[pathlib.Path] = None, - network_filter=None, - network_adjuster=None, - ) -> typing.List[str]: - return self.versioning_system.list_relevant_files(network_root) + def find_files(self, ignore_filters=False) -> typing.List[str]: + files = self.versioning_system.list_relevant_files(self.network_root_folder) + + if not ignore_filters: + if self.network_filter: + files = [file for file in files if file.startswith(self.network_filter)] + if self.network_prefix: + files = [self.network_prefix + file for file in files] + + return files -def select_network_finder(versioning_system: VersioningSystemInterface): - return NetworkFinder(versioning_system) +def select_network_finder( + versioning_system: VersioningSystemInterface, + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], + network_root_folder: pathlib.Path, +): + return NetworkFinder( + versioning_system, + network_filter, + network_prefix, + network_root_folder, + ) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index d687ab98..9ba8564f 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -36,10 +36,10 @@ def __init__( self.file_finder = file_finder self.disable_file_fixes = disable_file_fixes - def _produce_file_fixes_for_network( - self, network: typing.List[str] + def _produce_file_fixes( + self, files: typing.List[str] ) -> typing.List[UploadCollectionResultFileFixer]: - if not network or self.disable_file_fixes: + if not files or self.disable_file_fixes: return [] # patterns that we don't need to specify a reason for empty_line_regex = re.compile(r"^\s*$") @@ -94,7 +94,7 @@ def _produce_file_fixes_for_network( } result = [] - for filename in network: + for filename in files: for glob, fix_patterns in file_regex_patterns.items(): if fnmatch(filename, glob): result.append(self._get_file_fixes(filename, fix_patterns)) @@ -150,9 +150,9 @@ def generate_upload_data(self, report_type="coverage") -> UploadCollectionResult prep.run_preparation(self) logger.debug("Collecting relevant files") network = self.network_finder.find_files() - files = self.file_finder.find_files() - logger.info(f"Found {len(files)} {report_type} files to upload") - if not files: + report_files = self.file_finder.find_files() + logger.info(f"Found {len(report_files)} {report_type} files to report") + if not report_files: if report_type == "test_results": error_message = "No JUnit XML reports found. Please review our documentation (https://docs.codecov.com/docs/test-result-ingestion-beta) to generate and upload the file." else: @@ -163,13 +163,13 @@ def generate_upload_data(self, report_type="coverage") -> UploadCollectionResult fg="red", ) ) - for file in files: + for file in report_files: logger.info(f"> {file}") return UploadCollectionResult( network=network, - files=files, + files=report_files, file_fixes=( - self._produce_file_fixes_for_network(network) + self._produce_file_fixes(self.network_finder.find_files(True)) if report_type == "coverage" else [] ), diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index cac7a116..10d2a52d 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -118,6 +118,14 @@ def test_upload_process_options(mocker): " The type of the file to upload, coverage by", " default. Possible values are: testing,", " coverage.", + " --network-filter TEXT Specify a filter on the files listed in the", + " network section of the Codecov report. This", + " will only add files whose path begin with the", + " specified filter. Useful for upload-specific", + " path fixing", + " --network-prefix TEXT Specify a prefix on files listed in the", + " network section of the Codecov report. Useful", + " to help resolve path fixing", " --parent-sha TEXT SHA (with 40 chars) of what should be the", " parent of this commit", " -h, --help Show this message and exit.", diff --git a/tests/helpers/test_network_finder.py b/tests/helpers/test_network_finder.py index b7881812..859abc7d 100644 --- a/tests/helpers/test_network_finder.py +++ b/tests/helpers/test_network_finder.py @@ -6,11 +6,46 @@ def test_find_files(mocker, tmp_path): + filenames = ["a.txt", "b.txt"] + filtered_filenames = [] - expected_filenames = ["a.txt", "b.txt"] + mocked_vs = MagicMock() + mocked_vs.list_relevant_files.return_value = filenames + + assert NetworkFinder(versioning_system=mocked_vs, network_filter=None, network_prefix=None, network_root_folder=tmp_path).find_files() == filenames + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(False) == filtered_filenames + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + mocked_vs.list_relevant_files.assert_called_with(tmp_path) + +def test_find_files_with_filter(mocker, tmp_path): + filenames = ["hello/a.txt", "hello/c.txt", "bello/b.txt"] + filtered_filenames = ["hello/a.txt", "hello/c.txt"] + + mocked_vs = MagicMock() + mocked_vs.list_relevant_files.return_value = filenames + + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix=None, network_root_folder=tmp_path).find_files() == filtered_filenames + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + mocked_vs.list_relevant_files.assert_called_with(tmp_path) + +def test_find_files_with_prefix(mocker, tmp_path): + filenames = ["hello/a.txt", "hello/c.txt", "bello/b.txt"] + filtered_filenames = ["hellohello/a.txt", "hellohello/c.txt", "hellobello/b.txt"] + + mocked_vs = MagicMock() + mocked_vs.list_relevant_files.return_value = filenames + + assert NetworkFinder(versioning_system=mocked_vs, network_filter=None, network_prefix="hello", network_root_folder=tmp_path).find_files() == filtered_filenames + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + mocked_vs.list_relevant_files.assert_called_with(tmp_path) + +def test_find_files_with_filter_and_prefix(mocker, tmp_path): + filenames = ["hello/a.txt", "hello/c.txt", "bello/b.txt"] + filtered_filenames = ["bellohello/a.txt", "bellohello/c.txt"] mocked_vs = MagicMock() - mocked_vs.list_relevant_files.return_value = expected_filenames + mocked_vs.list_relevant_files.return_value = filenames - assert NetworkFinder(mocked_vs).find_files(tmp_path) == expected_filenames + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files() == filtered_filenames + assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames mocked_vs.list_relevant_files.assert_called_with(tmp_path) diff --git a/tests/services/upload/test_upload_collector.py b/tests/services/upload/test_upload_collector.py index 0df1c0bc..183ff8db 100644 --- a/tests/services/upload/test_upload_collector.py +++ b/tests/services/upload/test_upload_collector.py @@ -13,7 +13,7 @@ def test_fix_kt_files(): col = UploadCollector(None, None, None) - fixes = col._produce_file_fixes_for_network([str(kt_file)]) + fixes = col._produce_file_fixes([kt_file]) assert len(fixes) == 1 fixes_for_kt_file = fixes[0] @@ -33,7 +33,7 @@ def test_fix_go_files(): col = UploadCollector(None, None, None) - fixes = col._produce_file_fixes_for_network([str(go_file)]) + fixes = col._produce_file_fixes([go_file]) assert len(fixes) == 1 fixes_for_go_file = fixes[0] @@ -59,7 +59,7 @@ def test_fix_bad_encoding_files(mock_open): col = UploadCollector(None, None, None) - fixes = col._produce_file_fixes_for_network([str(go_file)]) + fixes = col._produce_file_fixes([go_file]) assert len(fixes) == 1 fixes_for_go_file = fixes[0] assert fixes_for_go_file.eof is None @@ -72,7 +72,7 @@ def test_fix_php_files(): col = UploadCollector(None, None, None) - fixes = col._produce_file_fixes_for_network([str(php_file)]) + fixes = col._produce_file_fixes([php_file]) assert len(fixes) == 1 fixes_for_php_file = fixes[0] @@ -87,7 +87,7 @@ def test_fix_for_cpp_swift_vala(tmp_path): col = UploadCollector(None, None, None) - fixes = col._produce_file_fixes_for_network([str(cpp_file)]) + fixes = col._produce_file_fixes([cpp_file]) assert len(fixes) == 1 fixes_for_cpp_file = fixes[0] @@ -109,7 +109,7 @@ def test_fix_when_disabled_fixes(tmp_path): col = UploadCollector(None, None, None, True) - fixes = col._produce_file_fixes_for_network([str(cpp_file)]) + fixes = col._produce_file_fixes([cpp_file]) assert len(fixes) == 0 assert fixes == [] @@ -164,7 +164,7 @@ def test_generate_upload_data(tmp_path): file_finder = FileFinder(tmp_path) - network_finder = NetworkFinder(GitVersioningSystem()) + network_finder = NetworkFinder(GitVersioningSystem(), None, None, None) collector = UploadCollector([], network_finder, file_finder) diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 9f7dbf61..8f7ad3e4 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -56,6 +56,8 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): env_vars=None, flags=None, name="name", + network_filter=None, + network_prefix=None, network_root_folder=None, files_search_root_folder=None, files_search_exclude_folders=None, @@ -81,7 +83,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system) + mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) mock_generate_upload_data.assert_called_with("coverage") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, @@ -144,6 +146,8 @@ def test_do_upload_logic_happy_path(mocker): env_vars=None, flags=None, name="name", + network_filter=None, + network_prefix=None, network_root_folder=None, files_search_root_folder=None, files_search_exclude_folders=None, @@ -168,7 +172,7 @@ def test_do_upload_logic_happy_path(mocker): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system) + mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) mock_generate_upload_data.assert_called_with("coverage") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, @@ -227,6 +231,8 @@ def test_do_upload_logic_dry_run(mocker): env_vars=None, flags=None, name="name", + network_filter=None, + network_prefix=None, network_root_folder=None, files_search_root_folder=None, files_search_exclude_folders=None, @@ -242,7 +248,7 @@ def test_do_upload_logic_dry_run(mocker): ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system) + mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) assert mock_generate_upload_data.call_count == 1 assert mock_send_upload_data.call_count == 0 mock_select_preparation_plugins.assert_called_with( @@ -288,6 +294,8 @@ def test_do_upload_logic_verbose(mocker, use_verbose_option): env_vars=None, flags=None, name="name", + network_filter=None, + network_prefix=None, network_root_folder=None, files_search_root_folder=None, files_search_exclude_folders=None, @@ -363,6 +371,8 @@ def side_effect(*args, **kwargs): env_vars=None, flags=None, name="name", + network_filter=None, + network_prefix=None, network_root_folder=None, files_search_root_folder=None, files_search_exclude_folders=None, @@ -393,7 +403,7 @@ def side_effect(*args, **kwargs): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system) + mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) mock_generate_upload_data.assert_called_with("coverage") mock_upload_completion_call.assert_called_with( commit_sha="commit_sha", @@ -444,6 +454,8 @@ def side_effect(*args, **kwargs): env_vars=None, flags=None, name="name", + network_filter=None, + network_prefix=None, network_root_folder=None, files_search_root_folder=None, files_search_exclude_folders=None, @@ -465,7 +477,7 @@ def side_effect(*args, **kwargs): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system) + mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) mock_generate_upload_data.assert_called_with("coverage") @@ -509,7 +521,9 @@ def test_do_upload_logic_happy_path_test_results(mocker): env_vars=None, flags=None, name="name", - network_root_folder=None, + network_filter="some_dir", + network_prefix="hello/", + network_root_folder="root/", files_search_root_folder=None, files_search_exclude_folders=None, files_search_explicitly_listed_files=None, @@ -531,7 +545,7 @@ def test_do_upload_logic_happy_path_test_results(mocker): assert res == UploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_not_called mock_select_file_finder.assert_called_with(None, None, None, False, "test_results") - mock_select_network_finder.assert_called_with(versioning_system) + mock_select_network_finder.assert_called_with(versioning_system, network_filter="some_dir", network_prefix="hello/", network_root_folder="root/") mock_generate_upload_data.assert_called_with("test_results") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, From 3638b2ec0c385a73b5a15ff8dead8b2af2c7098b Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:17:14 +0200 Subject: [PATCH 063/128] chore: fix test asserts (#407) Changing wrong asserts in tests to the correct function. This would break in python 3.12, but we don't build the CLI for 3.12 (yet) --- tests/runners/test_pytest_standard_runner.py | 2 +- tests/services/static_analysis/test_analyse_file.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/runners/test_pytest_standard_runner.py b/tests/runners/test_pytest_standard_runner.py index aadbcd3f..489a0e0c 100644 --- a/tests/runners/test_pytest_standard_runner.py +++ b/tests/runners/test_pytest_standard_runner.py @@ -55,7 +55,7 @@ def test_warning_bad_config(self, mock_warning): ) runner = PytestStandardRunner(params) # Adding invalid config options emits a warning - assert mock_warning.called_with( + mock_warning.assert_called_with( "Config parameter 'some_missing_option' is unknonw." ) # Warnings don't change the config diff --git a/tests/services/static_analysis/test_analyse_file.py b/tests/services/static_analysis/test_analyse_file.py index 2b28397e..7a31e864 100644 --- a/tests/services/static_analysis/test_analyse_file.py +++ b/tests/services/static_analysis/test_analyse_file.py @@ -47,12 +47,12 @@ def test_sample_analysis(input_filename, output_filename): @patch("builtins.open") @patch("codecov_cli.services.staticanalysis.get_best_analyzer", return_value=None) -def test_analyse_file_no_analyser(mock_get_analyser, mock_open): - fake_contents = MagicMock() +def test_analyse_file_no_analyzer(mock_get_analyzer, mock_open): + fake_contents = MagicMock(name="fake_file_contents") file_name = MagicMock(actual_filepath="filepath") - mock_open.return_value.read.return_value = fake_contents + mock_open.return_value.__enter__.return_value.read.return_value = fake_contents config = {} res = analyze_file(config, file_name) assert res == None - assert mock_open.called_with("filepath", "rb") - assert mock_get_analyser.called_with(file_name, fake_contents) + mock_open.assert_called_with("filepath", "rb") + mock_get_analyzer.assert_called_with(file_name, fake_contents) From 4b14ca23ef30b8981c3b63c6d16d52a7e8ec436c Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Tue, 9 Apr 2024 13:19:56 -0400 Subject: [PATCH 064/128] Prepare release 0.5.0 (#408) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4c453dff..48fb6a5a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.4.9", + version="0.5.0", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 63df4a99eb7a6bdda4409d59b8fd3e6341e3fa2f Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:12:11 -0700 Subject: [PATCH 065/128] fix: add network args to upload-process (#412) --- codecov_cli/commands/upload_process.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index 2f5acd3e..10fa5847 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -64,6 +64,8 @@ def upload_process( env_vars=env_vars, flags=flags, name=name, + network_filter=network_filter, + network_prefix=network_prefix, network_root_folder=network_root_folder, files_search_root_folder=files_search_root_folder, files_search_exclude_folders=files_search_exclude_folders, @@ -113,6 +115,8 @@ def upload_process( env_vars=env_vars, flags=flags, name=name, + network_filter=network_filter, + network_prefix=network_prefix, network_root_folder=network_root_folder, files_search_root_folder=files_search_root_folder, files_search_exclude_folders=files_search_exclude_folders, From aa5aa3f803e568f3bc86e8ee43c784bf58bbbb1b Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Mon, 15 Apr 2024 13:29:20 -0400 Subject: [PATCH 066/128] Prepare release 0.5.1 (#417) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48fb6a5a..22e60545 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.5.0", + version="0.5.1", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From f18f2d520f059476b513ef3b048bdcfc053d43aa Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:05:14 -0700 Subject: [PATCH 067/128] Th/create default name (#421) * fix: add some logs and test * fix: verbose * fix: maybe get more data * fix: add name to fallbacks * fix: default to build_code * fix: update the option * fix: cleanup * fix: cleanup again * fix: last cleanup --- .github/workflows/ci.yml | 4 ++-- codecov_cli/commands/upload.py | 2 ++ codecov_cli/fallbacks.py | 10 +++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb55e118..b0da70b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,8 +80,8 @@ jobs: - name: Dogfooding codecov-cli if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} run: | - codecovcli do-upload --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} - codecovcli do-upload --report-type test_results --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} + codecovcli -v do-upload --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} + codecovcli do-upload --report-type test_results --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} static-analysis: runs-on: ubuntu-latest diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index aa9e7f75..377553c2 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -98,6 +98,8 @@ def _turn_env_vars_into_dict(ctx, params, value): "-n", "--name", help="Custom defined name of the upload. Visible in Codecov UI", + cls=CodecovOption, + fallback_field=FallbackFieldEnum.build_code, ), click.option( "-B", diff --git a/codecov_cli/fallbacks.py b/codecov_cli/fallbacks.py index d84c6eb6..2c8f9b33 100644 --- a/codecov_cli/fallbacks.py +++ b/codecov_cli/fallbacks.py @@ -5,15 +5,15 @@ class FallbackFieldEnum(Enum): - commit_sha = auto() - build_url = auto() + branch = auto() build_code = auto() + build_url = auto() + commit_sha = auto() + git_service = auto() job_code = auto() pull_request_number = auto() - slug = auto() - branch = auto() service = auto() - git_service = auto() + slug = auto() class CodecovOption(click.Option): From 3abcc36ffdbe47af82e8a33031183e154dc1c4e7 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 17 Apr 2024 12:23:45 -0400 Subject: [PATCH 068/128] Prepare release 0.5.2 (#422) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 22e60545..a73c53a7 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.5.1", + version="0.5.2", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 241f9990b917d8c5b40e855a9d5389cf4e475056 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:51:52 -0400 Subject: [PATCH 069/128] feat: add process-test-results command (#424) * feat: add process-test-results command this command locally processes test result files and makes a call to the GH API to create a comment This command should be run in Github Actions, it expects the provider-token option to contain the contents of the GITHUB_TOKEN env var. * deps: update requirements.txt * fix: use typing List instead of list Signed-off-by: joseph-sentry --- .github/workflows/ci.yml | 35 +++ codecov_cli/commands/process_test_results.py | 175 +++++++++++++++ codecov_cli/main.py | 2 + requirements.txt | 12 +- samples/junit.xml | 19 ++ setup.py | 2 + tests/commands/test_process_test_results.py | 224 +++++++++++++++++++ tests/test_codecov_cli.py | 1 + 8 files changed, 464 insertions(+), 6 deletions(-) create mode 100644 codecov_cli/commands/process_test_results.py create mode 100644 samples/junit.xml create mode 100644 tests/commands/test_process_test_results.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0da70b3..05a0c615 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,3 +132,38 @@ jobs: - name: Upload smart-labels run: | codecovcli --codecov-yml-path=codecov.yml do-upload --plugin pycoverage --plugin compress-pycoverage --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --flag smart-labels + + test-process-test-results-cmd: + runs-on: ubuntu-latest + permissions: + pull-requests: write + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.11" + - python-version: "3.10" + - python-version: "3.9" + - python-version: "3.8" + steps: + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 2 + - name: Set up Python ${{matrix.python-version}} + uses: actions/setup-python@v3 + with: + python-version: "${{matrix.python-version}}" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + python setup.py develop + pip install -r tests/requirements.txt + - name: Test with pytest + run: | + pytest --cov --junitxml=junit.xml + - name: Dogfooding codecov-cli + if: ${{ !cancelled() }} + run: | + codecovcli process-test-results --provider-token ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/codecov_cli/commands/process_test_results.py b/codecov_cli/commands/process_test_results.py new file mode 100644 index 00000000..1bf4558a --- /dev/null +++ b/codecov_cli/commands/process_test_results.py @@ -0,0 +1,175 @@ +import logging +import os +import pathlib +from dataclasses import dataclass +from typing import List + +import click +from test_results_parser import ( + Outcome, + ParserError, + Testrun, + build_message, + parse_junit_xml, +) + +from codecov_cli.helpers.request import ( + log_warnings_and_errors_if_any, + send_post_request, +) +from codecov_cli.services.upload.file_finder import select_file_finder + +logger = logging.getLogger("codecovcli") + + +_process_test_results_options = [ + click.option( + "-s", + "--dir", + "--files-search-root-folder", + "dir", + help="Folder where to search for test results files", + type=click.Path(path_type=pathlib.Path), + default=pathlib.Path.cwd, + show_default="Current Working Directory", + ), + click.option( + "-f", + "--file", + "--files-search-direct-file", + "files", + help="Explicit files to upload. These will be added to the test results files to be processed. If you wish to only process the specified files, please consider using --disable-search to disable processing other files.", + type=click.Path(path_type=pathlib.Path), + multiple=True, + default=[], + ), + click.option( + "--exclude", + "--files-search-exclude-folder", + "exclude_folders", + help="Folders to exclude from search", + type=click.Path(path_type=pathlib.Path), + multiple=True, + default=[], + ), + click.option( + "--disable-search", + help="Disable search for coverage files. This is helpful when specifying what files you want to upload with the --file option.", + is_flag=True, + default=False, + ), + click.option( + "--provider-token", + help="Token used to make calls to Repo provider API", + type=str, + default=None, + ), +] + + +def process_test_results_options(func): + for option in reversed(_process_test_results_options): + func = option(func) + return func + + +@dataclass +class TestResultsNotificationPayload: + failures: List[Testrun] + failed: int = 0 + passed: int = 0 + skipped: int = 0 + + +@click.command() +@process_test_results_options +def process_test_results( + dir=None, files=None, exclude_folders=None, disable_search=None, provider_token=None +): + if provider_token is None: + raise click.ClickException( + "Provider token was not provided. Make sure to pass --provider-token option with the contents of the GITHUB_TOKEN secret, so we can make a comment." + ) + + summary_file_path = os.getenv("GITHUB_STEP_SUMMARY") + if summary_file_path is None: + raise click.ClickException( + "Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable." + ) + + slug = os.getenv("GITHUB_REPOSITORY") + if slug is None: + raise click.ClickException( + "Error getting repo slug from environment. Can't find GITHUB_REPOSITORY environment variable." + ) + + ref = os.getenv("GITHUB_REF") + if ref is None or "pull" not in ref: + raise click.ClickException( + "Error getting PR number from environment. Can't find GITHUB_REF environment variable." + ) + + file_finder = select_file_finder( + dir, exclude_folders, files, disable_search, report_type="test_results" + ) + + upload_collection_results = file_finder.find_files() + if len(upload_collection_results) == 0: + raise click.ClickException( + "No JUnit XML files were found. Make sure to specify them using the --file option." + ) + + payload = generate_message_payload(upload_collection_results) + + message = build_message(payload) + + # write to step summary file + with open(summary_file_path, "w") as f: + f.write(message) + + # GITHUB_REF is documented here: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + pr_number = ref.split("/")[2] + + create_github_comment(provider_token, slug, pr_number, message) + + +def create_github_comment(token, repo_slug, pr_number, message): + url = f"https://api.github.com/repos/{repo_slug}/issues/{pr_number}/comments" + + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + } + logger.info("Posting github comment") + + log_warnings_and_errors_if_any( + send_post_request(url=url, data={"body": message}, headers=headers), + "Posting test results comment", + ) + + +def generate_message_payload(upload_collection_results): + payload = TestResultsNotificationPayload(failures=[]) + + for result in upload_collection_results: + testruns = [] + try: + logger.info(f"Parsing {result.get_filename()}") + testruns = parse_junit_xml(result.get_content()) + for testrun in testruns: + if ( + testrun.outcome == Outcome.Failure + or testrun.outcome == Outcome.Error + ): + payload.failed += 1 + payload.failures.append(testrun) + elif testrun.outcome == Outcome.Skip: + payload.skipped += 1 + else: + payload.passed += 1 + except ParserError as err: + raise click.ClickException( + f"Error parsing {str(result.get_filename(), 'utf8')} with error: {err}" + ) + return payload diff --git a/codecov_cli/main.py b/codecov_cli/main.py index 1c364d36..bd55d071 100644 --- a/codecov_cli/main.py +++ b/codecov_cli/main.py @@ -11,6 +11,7 @@ from codecov_cli.commands.empty_upload import empty_upload from codecov_cli.commands.get_report_results import get_report_results from codecov_cli.commands.labelanalysis import label_analysis +from codecov_cli.commands.process_test_results import process_test_results from codecov_cli.commands.report import create_report from codecov_cli.commands.send_notifications import send_notifications from codecov_cli.commands.staticanalysis import static_analysis @@ -71,6 +72,7 @@ def cli( cli.add_command(empty_upload) cli.add_command(upload_process) cli.add_command(send_notifications) +cli.add_command(process_test_results) def run(): diff --git a/requirements.txt b/requirements.txt index 9fbe99a8..d40822af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile setup.py @@ -15,8 +15,6 @@ charset-normalizer==3.3.0 # via requests click==8.1.7 # via codecov-cli (setup.py) -exceptiongroup==1.1.3 - # via anyio h11==0.14.0 # via httpcore httpcore==0.16.3 @@ -32,19 +30,21 @@ ijson==3.2.3 # via codecov-cli (setup.py) pyyaml==6.0.1 # via codecov-cli (setup.py) +regex==2023.12.25 + # via codecov-cli (setup.py) requests==2.31.0 # via responses responses==0.21.0 # via codecov-cli (setup.py) rfc3986[idna2008]==1.5.0 - # via - # httpx - # rfc3986 + # via httpx sniffio==1.3.0 # via # anyio # httpcore # httpx +test-results-parser==0.1.0 + # via codecov-cli (setup.py) tree-sitter==0.20.2 # via codecov-cli (setup.py) urllib3==2.0.6 diff --git a/samples/junit.xml b/samples/junit.xml new file mode 100644 index 00000000..e698e007 --- /dev/null +++ b/samples/junit.xml @@ -0,0 +1,19 @@ + + + + + + + + def + test_divide(): + > assert Calculator.divide(1, 2) == 0.5 + E assert 1.0 == 0.5 + E + where 1.0 = <function Calculator.divide at 0x104c9eb90>(1, 2) + E + where <function Calculator.divide at 0x104c9eb90> = Calculator.divide + api/temp/calculator/test_calculator.py:30: AssertionError + + + \ No newline at end of file diff --git a/setup.py b/setup.py index a73c53a7..7b3b488d 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,8 @@ "pyyaml==6.*", "responses==0.21.*", "tree-sitter==0.20.*", + "test-results-parser==0.1.*", + "regex", ], entry_points={ "console_scripts": [ diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py new file mode 100644 index 00000000..d0c62eb7 --- /dev/null +++ b/tests/commands/test_process_test_results.py @@ -0,0 +1,224 @@ +import logging +import os + +from click.testing import CliRunner + +from codecov_cli.main import cli +from codecov_cli.types import RequestResult + + +def test_process_test_results( + mocker, + tmpdir, +): + + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_REF": "pull/fake/pull", + "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, + }, + ) + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--provider-token", + "whatever", + "--file", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 0 + + + mocked_post.assert_called_with( + url="https://api.github.com/repos/fake/repo/issues/pull/comments", + data={ + "body": "### :x: Failed Test Results: \nCompleted 4 tests with **`1 failed`**, 3 passed and 0 skipped.\n
View the full list of failed tests\n\n| **Test Description** | **Failure message** |\n| :-- | :-- |\n|
Testsuite:
api.temp.calculator.test_calculator::test_divide

Test name:
pytest
|
def
test_divide():
&gt; assert Calculator.divide(1, 2) == 0.5
E assert 1.0 == 0.5
E + where 1.0 = &lt;function Calculator.divide at 0x104c9eb90&gt;(1, 2)
E + where &lt;function Calculator.divide at 0x104c9eb90&gt; = Calculator.divide
.../temp/calculator/test_calculator.py:30: AssertionError
|" + }, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": "Bearer whatever", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) + + + +def test_process_test_results_non_existent_file(mocker, tmpdir): + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_REF": "pull/fake/pull", + "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, + }, + ) + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--provider-token", + "whatever", + "--file", + "samples/fake.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 1 + expected_logs = [ + "ci service found", + 'Some files were not found', + ] + for log in expected_logs: + assert log in result.output + + +def test_process_test_results_missing_repo(mocker, tmpdir): + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REF": "pull/fake/pull", + "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, + }, + ) + if "GITHUB_REPOSITORY" in os.environ: + del os.environ["GITHUB_REPOSITORY"] + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--provider-token", + "whatever", + "--file", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 1 + expected_logs = [ + "ci service found", + "Error: Error getting repo slug from environment. Can't find GITHUB_REPOSITORY environment variable.", + ] + for log in expected_logs: + assert log in result.output + + +def test_process_test_results_missing_ref(mocker, tmpdir): + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, + }, + ) + + if "GITHUB_REF" in os.environ: + del os.environ["GITHUB_REF"] + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--provider-token", + "whatever", + "--file", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 1 + expected_logs = [ + "ci service found", + "Error: Error getting PR number from environment. Can't find GITHUB_REF environment variable.", + ] + for log in expected_logs: + assert log in result.output + + + +def test_process_test_results_missing_step_summary(mocker, tmpdir): + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_REF": "pull/fake/pull", + }, + ) + if "GITHUB_STEP_SUMMARY" in os.environ: + del os.environ["GITHUB_STEP_SUMMARY"] + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--provider-token", + "whatever", + "--file", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 1 + expected_logs = [ + "ci service found", + "Error: Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable.", + ] + for log in expected_logs: + assert log in result.output \ No newline at end of file diff --git a/tests/test_codecov_cli.py b/tests/test_codecov_cli.py index 5bf650db..80136f06 100644 --- a/tests/test_codecov_cli.py +++ b/tests/test_codecov_cli.py @@ -11,6 +11,7 @@ def test_existing_commands(): "get-report-results", "label-analysis", "pr-base-picking", + "process-test-results", "send-notifications", "static-analysis", "upload-process", From 88b7c7125fef68fe2e8fbf1d94a1feec81e173d3 Mon Sep 17 00:00:00 2001 From: Espen Carlsen Date: Fri, 26 Apr 2024 16:00:05 +0200 Subject: [PATCH 070/128] fix: add support for ssh url repos (#416) Co-authored-by: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Co-authored-by: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> --- codecov_cli/helpers/git.py | 5 ++++- tests/helpers/test_git.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index 2b4fcde6..89ae8d40 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -61,13 +61,16 @@ def parse_git_service(remote_repo_url: str): Possible cases we're considering: - https://github.com/codecov/codecov-cli.git returns github - git@github.com:codecov/codecov-cli.git returns github + - ssh://git@github.com/gitcodecov/codecov-cli returns github + - ssh://git@github.com:gitcodecov/codecov-cli returns github - https://user-name@bitbucket.org/namespace-codecov/first_repo.git returns bitbucket """ services = [service.value for service in GitService] parsed_url = urlparse(remote_repo_url) service = None - if remote_repo_url.startswith("https://"): + scheme = parsed_url.scheme + if scheme in ("https", "ssh"): netloc = parsed_url.netloc if "@" in netloc: netloc = netloc.split("@", 1)[1] diff --git a/tests/helpers/test_git.py b/tests/helpers/test_git.py index 155a33c2..f0415547 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -53,6 +53,7 @@ ("ssh://host.abc.xz/owner/repo.git", "owner/repo"), ("user-name@host.xz:owner/repo.git/", "owner/repo"), ("host.xz:owner/repo.git/", "owner/repo"), + ("ssh://git@github.com/gitcodecov/codecov-cli", "gitcodecov/codecov-cli"), ], ) def test_parse_slug_valid_address(address, slug): @@ -107,6 +108,8 @@ def test_parse_slug_invalid_address(address): "bitbucket", ), ("git@bitbucket.org:name-codecov/abc.git.git", "bitbucket"), + ("ssh://git@github.com/gitcodecov/codecov-cli", "github"), + ("ssh://git@github.com:gitcodecov/codecov-cli", "github"), ], ) def test_parse_git_service_valid_address(address, git_service): From e4b6e1ccdec2bb68d8286a7e1f9225bb9ee48437 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Tue, 7 May 2024 21:35:08 +0700 Subject: [PATCH 071/128] fix: log info if a file shows up as a directory (#430) * fix: log info if a file shows up as a directory * fix: black * fix: lint --- codecov_cli/helpers/folder_searcher.py | 2 +- codecov_cli/helpers/git_services/github.py | 8 +++++--- codecov_cli/helpers/versioning_systems.py | 8 +++++--- codecov_cli/services/staticanalysis/analyzers/general.py | 4 ++-- codecov_cli/services/upload/upload_collector.py | 2 ++ 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/codecov_cli/helpers/folder_searcher.py b/codecov_cli/helpers/folder_searcher.py index 6abbd97b..19e1be46 100644 --- a/codecov_cli/helpers/folder_searcher.py +++ b/codecov_cli/helpers/folder_searcher.py @@ -58,7 +58,7 @@ def search_files( this_is_excluded = functools.partial( _is_excluded, filename_exclude_regex, multipart_exclude_regex ) - for (dirpath, dirnames, filenames) in os.walk(folder_to_search): + for dirpath, dirnames, filenames in os.walk(folder_to_search): dirs_to_remove = set(d for d in dirnames if d in folders_to_ignore) if multipart_exclude_regex is not None: diff --git a/codecov_cli/helpers/git_services/github.py b/codecov_cli/helpers/git_services/github.py index 2e7551bb..5aeb1744 100644 --- a/codecov_cli/helpers/git_services/github.py +++ b/codecov_cli/helpers/git_services/github.py @@ -24,9 +24,11 @@ def get_pull_request(self, slug, pr_number) -> PullDict: "ref": res["head"]["ref"], # Through empiric test data it seems that the "repo" key in "head" is set to None # If the PR is from the same repo (e.g. not from a fork) - "slug": res["head"]["repo"]["full_name"] - if res["head"]["repo"] - else res["base"]["repo"]["full_name"], + "slug": ( + res["head"]["repo"]["full_name"] + if res["head"]["repo"] + else res["base"]["repo"]["full_name"] + ), }, "base": { "sha": res["base"]["sha"], diff --git a/codecov_cli/helpers/versioning_systems.py b/codecov_cli/helpers/versioning_systems.py index 07763876..b6a53df2 100644 --- a/codecov_cli/helpers/versioning_systems.py +++ b/codecov_cli/helpers/versioning_systems.py @@ -135,9 +135,11 @@ def list_relevant_files( ) return [ - filename[1:-1] - if filename.startswith('"') and filename.endswith('"') - else filename + ( + filename[1:-1] + if filename.startswith('"') and filename.endswith('"') + else filename + ) for filename in res.stdout.decode("unicode_escape").strip().split("\n") ] diff --git a/codecov_cli/services/staticanalysis/analyzers/general.py b/codecov_cli/services/staticanalysis/analyzers/general.py index b093fc7b..c0554f73 100644 --- a/codecov_cli/services/staticanalysis/analyzers/general.py +++ b/codecov_cli/services/staticanalysis/analyzers/general.py @@ -85,13 +85,13 @@ def _get_parent_chain(self, node): def get_import_lines(self, root_node, imports_query): import_lines = set() - for (a, _) in imports_query.captures(root_node): + for a, _ in imports_query.captures(root_node): import_lines.add((a.start_point[0] + 1, a.end_point[0] - a.start_point[0])) return import_lines def get_definition_lines(self, root_node, definitions_query): definition_lines = set() - for (a, _) in definitions_query.captures(root_node): + for a, _ in definitions_query.captures(root_node): definition_lines.add( (a.start_point[0] + 1, a.end_point[0] - a.start_point[0]) ) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 9ba8564f..884438ec 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -139,6 +139,8 @@ def _get_file_fixes( reason=err.reason, ), ) + except IsADirectoryError as err: + logger.info(f"Skipping {filename}, found a directory not a file") return UploadCollectionResultFileFixer( path, fixed_lines_without_reason, fixed_lines_with_reason, eof From 3c2bac8ff016c2dcf790d055e85ad2b76c28fb35 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Tue, 7 May 2024 21:45:23 +0700 Subject: [PATCH 072/128] fix: file fix for lines with just a pren (#431) * fix: file fix for lines with just a pren * fix: lint * fix: black * fix: paren to parenthesis --- codecov_cli/services/upload/upload_collector.py | 4 ++-- tests/data/files_to_fix_examples/sample.kt | 3 +++ tests/services/upload/test_upload_collector.py | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 884438ec..47a99670 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -46,6 +46,7 @@ def _produce_file_fixes( comment_regex = re.compile(r"^\s*\/\/.*$") bracket_regex = re.compile(r"^\s*[\{\}]\s*(\/\/.*)?$") list_regex = re.compile(r"^\s*[\]\[]\s*(\/\/.*)?$") + parenthesis_regex = re.compile(r"^\s*[\(\)]\s*(\/\/.*)?$") go_function_regex = re.compile(r"^\s*func\s*[\{]\s*(\/\/.*)?$") php_end_bracket_regex = re.compile(r"^\s*\);\s*(\/\/.*)?$") @@ -54,7 +55,7 @@ def _produce_file_fixes( lcov_excel_regex = re.compile(r"\/\/ LCOV_EXCL") kt_patterns_to_apply = fix_patterns_to_apply( - [bracket_regex], [comment_block_regex], True + [bracket_regex, parenthesis_regex], [comment_block_regex], True ) go_patterns_to_apply = fix_patterns_to_apply( [empty_line_regex, comment_regex, bracket_regex, go_function_regex], @@ -71,7 +72,6 @@ def _produce_file_fixes( [], False, ) - cpp_swift_vala_patterns_to_apply = fix_patterns_to_apply( [empty_line_regex, bracket_regex], [lcov_excel_regex], diff --git a/tests/data/files_to_fix_examples/sample.kt b/tests/data/files_to_fix_examples/sample.kt index df8d524f..6793cb59 100644 --- a/tests/data/files_to_fix_examples/sample.kt +++ b/tests/data/files_to_fix_examples/sample.kt @@ -13,6 +13,9 @@ secnod line should fix previous } +data class Key( + val key: String? = null +) /* diff --git a/tests/services/upload/test_upload_collector.py b/tests/services/upload/test_upload_collector.py index 183ff8db..a433b904 100644 --- a/tests/services/upload/test_upload_collector.py +++ b/tests/services/upload/test_upload_collector.py @@ -18,12 +18,12 @@ def test_fix_kt_files(): assert len(fixes) == 1 fixes_for_kt_file = fixes[0] - assert fixes_for_kt_file.eof == 30 - assert fixes_for_kt_file.fixed_lines_without_reason == set([1, 3, 7, 9, 12, 14]) + assert fixes_for_kt_file.eof == 33 + assert fixes_for_kt_file.fixed_lines_without_reason == set([1, 3, 7, 9, 12, 14, 18]) assert fixes_for_kt_file.fixed_lines_with_reason == set( [ - (17, " /*\n"), - (22, "*/\n"), + (20, " /*\n"), + (25, "*/\n"), ] ) From 4de8fc24b865bf71c9d6b3cac9b1379fafa11883 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Tue, 7 May 2024 21:52:36 +0700 Subject: [PATCH 073/128] fix: allow for .. explicit file search (#432) --- codecov_cli/services/upload/file_finder.py | 6 ++++- .../upload/test_coverage_file_finder.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index a1fb3be2..79e845e3 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -253,7 +253,11 @@ def get_user_specified_files(self, regex_patterns_to_exclude): user_files_paths_resolved = [path.resolve() for path in user_files_paths] for filepath in self.explicitly_listed_files: if filepath.resolve() not in user_files_paths_resolved: - not_found_files.append(filepath) + ## The file given might be linked or in a parent dir, check to see if it exists + if filepath.exists(): + user_files_paths.append(filepath) + else: + not_found_files.append(filepath) if not_found_files: logger.warning( diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index 6b67cd38..afe717ff 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -206,6 +206,29 @@ def test_find_coverage_files_with_existing_files( expected_paths = sorted([file.get_filename() for file in expected]) assert result == expected_paths + def test_find_coverage_files_with_file_in_parent( + self, coverage_file_finder_fixture + ): + # Create some sample coverage coverage_file_finder_fixture + ( + project_root, + coverage_file_finder, + ) = coverage_file_finder_fixture + coverage_files = [ + project_root.parent / "coverage.xml", + ] + for file in coverage_files: + file.touch() + + coverage_file_finder.explicitly_listed_files = [project_root.parent / "coverage.xml"] + + result = sorted( + [file.get_filename() for file in coverage_file_finder.find_files()] + ) + expected = [UploadCollectionResultFile(Path(f"{project_root.parent}/coverage.xml"))] + expected_paths = sorted([file.get_filename() for file in expected]) + assert result == expected_paths + def test_find_coverage_files_with_no_files(self, coverage_file_finder_fixture): ( _, From d3f982fb7f3f07377fef1acd0b71cd73fac811e9 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Tue, 7 May 2024 13:43:28 -0400 Subject: [PATCH 074/128] Prepare release 0.6.0 (#436) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7b3b488d..de949752 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.5.2", + version="0.6.0", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 7432bad76c619e34d6617b578e039e7d733e3f00 Mon Sep 17 00:00:00 2001 From: Espen Carlsen Date: Tue, 14 May 2024 15:39:31 +0200 Subject: [PATCH 075/128] feat: add Google Cloud Build CI support (#418) --- codecov_cli/helpers/ci_adapters/__init__.py | 2 + codecov_cli/helpers/ci_adapters/cloudbuild.py | 70 ++++++ tests/ci_adapters/test_cloudbuild.py | 213 ++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 codecov_cli/helpers/ci_adapters/cloudbuild.py create mode 100644 tests/ci_adapters/test_cloudbuild.py diff --git a/codecov_cli/helpers/ci_adapters/__init__.py b/codecov_cli/helpers/ci_adapters/__init__.py index 460d78e6..26215d86 100644 --- a/codecov_cli/helpers/ci_adapters/__init__.py +++ b/codecov_cli/helpers/ci_adapters/__init__.py @@ -7,6 +7,7 @@ from codecov_cli.helpers.ci_adapters.buildkite import BuildkiteAdapter from codecov_cli.helpers.ci_adapters.circleci import CircleCICIAdapter from codecov_cli.helpers.ci_adapters.cirrus_ci import CirrusCIAdapter +from codecov_cli.helpers.ci_adapters.cloudbuild import GoogleCloudBuildAdapter from codecov_cli.helpers.ci_adapters.codebuild import AWSCodeBuildCIAdapter from codecov_cli.helpers.ci_adapters.droneci import DroneCIAdapter from codecov_cli.helpers.ci_adapters.github_actions import GithubActionsCIAdapter @@ -54,6 +55,7 @@ def get_ci_providers_list(): TeamcityAdapter(), TravisCIAdapter(), AWSCodeBuildCIAdapter(), + GoogleCloudBuildAdapter(), # local adapter should always be the last one LocalAdapter(), ] diff --git a/codecov_cli/helpers/ci_adapters/cloudbuild.py b/codecov_cli/helpers/ci_adapters/cloudbuild.py new file mode 100644 index 00000000..0f52a2e2 --- /dev/null +++ b/codecov_cli/helpers/ci_adapters/cloudbuild.py @@ -0,0 +1,70 @@ +import os + +from codecov_cli.helpers.ci_adapters.base import CIAdapterBase + + +class GoogleCloudBuildAdapter(CIAdapterBase): + """ + Google Cloud Build uses variable substitutions in the builds + https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values + For these to be available as environment variables, so this adapter + can read the values, you have to manually map the substitution variables to + environment variables on the build step, like this + env: + - '_PR_NUMBER=$_PR_NUMBER' + - 'BRANCH_NAME=$BRANCH_NAME' + - 'BUILD_ID=$BUILD_ID' + - 'COMMIT_SHA=$COMMIT_SHA' + - 'LOCATION=$LOCATION' + - 'PROJECT_ID=$PROJECT_ID' + - 'PROJECT_NUMBER=$PROJECT_NUMBER' + - 'REF_NAME=$REF_NAME' + - 'REPO_FULL_NAME=$REPO_FULL_NAME' + - 'TRIGGER_NAME=$TRIGGER_NAME' + Read more about manual substitution mapping here: + https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#map_substitutions_manually + """ + + def detect(self) -> bool: + return all( + list( + map(os.getenv, ["LOCATION", "PROJECT_NUMBER", "PROJECT_ID", "BUILD_ID"]) + ) + ) + + def _get_branch(self): + return os.getenv("BRANCH_NAME") + + def _get_build_code(self): + return os.getenv("BUILD_ID") + + def _get_commit_sha(self): + return os.getenv("COMMIT_SHA") + + def _get_slug(self): + return os.getenv("REPO_FULL_NAME") + + def _get_build_url(self): + # to build the url, the environment variables LOCATION, PROJECT_ID and BUILD_ID are needed + if not all(list(map(os.getenv, ["LOCATION", "PROJECT_ID", "BUILD_ID"]))): + return None + + location = os.getenv("LOCATION") + project_id = os.getenv("PROJECT_ID") + build_id = os.getenv("BUILD_ID") + + return f"https://console.cloud.google.com/cloud-build/builds;region={location}/{build_id}?project={project_id}" + + def _get_pull_request_number(self): + pr_num = os.getenv("_PR_NUMBER") + return pr_num if pr_num != "" else None + + def _get_job_code(self): + job_code = os.getenv("TRIGGER_NAME") + return job_code if job_code != "" else None + + def _get_service(self): + return "google_cloud_build" + + def get_service_name(self): + return "GoogleCloudBuild" diff --git a/tests/ci_adapters/test_cloudbuild.py b/tests/ci_adapters/test_cloudbuild.py new file mode 100644 index 00000000..ec4b0889 --- /dev/null +++ b/tests/ci_adapters/test_cloudbuild.py @@ -0,0 +1,213 @@ +import os +from enum import Enum + +import pytest + +from codecov_cli.fallbacks import FallbackFieldEnum +from codecov_cli.helpers.ci_adapters.cloudbuild import GoogleCloudBuildAdapter + + +class CloudBuildEnvEnum(str, Enum): + BRANCH_NAME = "BRANCH_NAME" + BUILD_ID = "BUILD_ID" + COMMIT_SHA = "COMMIT_SHA" + LOCATION = "LOCATION" + PROJECT_ID = "PROJECT_ID" + PROJECT_NUMBER = "PROJECT_NUMBER" + REPO_FULL_NAME = "REPO_FULL_NAME" + _PR_NUMBER = "_PR_NUMBER" + TRIGGER_NAME = "TRIGGER_NAME" + + +class TestCloudBuild(object): + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, False), + ( + { + CloudBuildEnvEnum.LOCATION: "global", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + CloudBuildEnvEnum.PROJECT_NUMBER: "123", + }, + False, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + CloudBuildEnvEnum.PROJECT_NUMBER: "123", + }, + False, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.LOCATION: "global", + CloudBuildEnvEnum.PROJECT_NUMBER: "123", + }, + False, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.LOCATION: "global", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + }, + False, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.LOCATION: "global", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + CloudBuildEnvEnum.PROJECT_NUMBER: "123", + }, + True, + ), + ], + ) + def test_detect(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().detect() + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({CloudBuildEnvEnum.BRANCH_NAME: "abc"}, "abc"), + ], + ) + def test_branch(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value(FallbackFieldEnum.branch) + + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ( + {CloudBuildEnvEnum.BUILD_ID: "52cbb633-aca0-4289-90bd-76e4e60baf82"}, + "52cbb633-aca0-4289-90bd-76e4e60baf82", + ), + ], + ) + def test_build_code(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value( + FallbackFieldEnum.build_code + ) + + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ( + { + CloudBuildEnvEnum.LOCATION: "global", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + }, + None, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + }, + None, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.LOCATION: "global", + }, + None, + ), + ( + { + CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de", + CloudBuildEnvEnum.LOCATION: "global", + CloudBuildEnvEnum.PROJECT_ID: "my_project", + }, + "https://console.cloud.google.com/cloud-build/builds;region=global/fd02b20f-72a3-41b5-862d-2c15e5f289de?project=my_project", + ), + ], + ) + def test_build_url(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value( + FallbackFieldEnum.build_url + ) + + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({CloudBuildEnvEnum.COMMIT_SHA: "123456789000111"}, "123456789000111"), + ], + ) + def test_commit_sha(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value( + FallbackFieldEnum.commit_sha + ) + + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({CloudBuildEnvEnum.TRIGGER_NAME: ""}, None), + ({CloudBuildEnvEnum.TRIGGER_NAME: "build-job-name"}, "build-job-name"), + ], + ) + def test_job_code(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value( + FallbackFieldEnum.job_code + ) + + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({CloudBuildEnvEnum._PR_NUMBER: ""}, None), + ({CloudBuildEnvEnum._PR_NUMBER: "123"}, "123"), + ], + ) + def test_pull_request_number(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value( + FallbackFieldEnum.pull_request_number + ) + + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({CloudBuildEnvEnum.REPO_FULL_NAME: "owner/repo"}, "owner/repo"), + ], + ) + def test_slug(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = GoogleCloudBuildAdapter().get_fallback_value(FallbackFieldEnum.slug) + + assert actual == expected + + def test_service(self): + assert ( + GoogleCloudBuildAdapter().get_fallback_value(FallbackFieldEnum.service) + == "google_cloud_build" + ) From bd4f749af6d3a941a3ce6075f7bec44fe9dd7ca2 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 4 Jun 2024 09:45:23 -0400 Subject: [PATCH 076/128] fix: retry all 5xx status code errors (#452) Fixes: https://github.com/codecov/engineering-team/issues/1709 Signed-off-by: joseph-sentry --- codecov_cli/helpers/request.py | 4 ++-- tests/helpers/test_upload_sender.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index d356eed8..048efd23 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -57,9 +57,9 @@ def wrapper(*args, **kwargs): while retry < MAX_RETRIES: try: response = func(*args, **kwargs) - if response.status_code == 502: + if response.status_code >= 500: logger.warning( - "Response status code was 502.", + f"Response status code was {response.status_code}.", extra=dict(extra_log_attributes=dict(retry=retry)), ) raise RetryException diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 054974fc..19d6ddea 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -306,11 +306,13 @@ def test_upload_sender_result_fail_post_400( assert sender.warnings is not None - def test_upload_sender_result_fail_post_502( - self, mocker, mocked_responses, mocked_legacy_upload_endpoint, capsys + + @pytest.mark.parametrize("error_code", [500, 502]) + def test_upload_sender_result_fail_post_500s( + self, mocker, mocked_responses, mocked_legacy_upload_endpoint, capsys, error_code ): mocker.patch("codecov_cli.helpers.request.sleep") - mocked_legacy_upload_endpoint.status = 502 + mocked_legacy_upload_endpoint.status = error_code with pytest.raises(Exception, match="Request failed after too many retries"): _ = UploadSender().send_upload_data( @@ -318,7 +320,7 @@ def test_upload_sender_result_fail_post_502( ) matcher = re.compile( - r"(warning.*((Response status code was 502)|(Request failed\. Retrying)).*(\n)?){6}" + rf"(warning.*((Response status code was {error_code})|(Request failed\. Retrying)).*(\n)?){{6}}" ) assert matcher.match(capsys.readouterr().err) is not None From 24d49dd4a4e879687218faf97cfc6f5b9d83a413 Mon Sep 17 00:00:00 2001 From: Branch Vincent Date: Mon, 17 Jun 2024 04:15:04 -0700 Subject: [PATCH 077/128] support python 3.12 (#458) * fix: use raw strings for SyntaxWarning on 3.12 As show with: `python3.12 -m compileall -f $(git ls-files '*.py')` * ci: add python 3.12 thanks @branchvincent for these changes --- .github/workflows/ci.yml | 25 ++++++++++---------- codecov_cli/helpers/ci_adapters/codebuild.py | 12 +++++----- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05a0c615..f9d26e76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,9 @@ jobs: with: submodules: true fetch-depth: 2 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 with: - # Because of https://github.com/tree-sitter/py-tree-sitter/issues/162 - python-version: '3.11.x' + python-version: "3.12" - name: Install CLI run: | pip install codecov-cli @@ -55,6 +54,7 @@ jobs: fail-fast: false matrix: include: + - python-version: "3.12" - python-version: "3.11" - python-version: "3.10" - python-version: "3.9" @@ -65,14 +65,14 @@ jobs: submodules: true fetch-depth: 2 - name: Set up Python ${{matrix.python-version}} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: "${{matrix.python-version}}" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - python setup.py develop + python -m pip install -e . pip install -r tests/requirements.txt - name: Test with pytest run: | @@ -92,10 +92,9 @@ jobs: with: submodules: true fetch-depth: 2 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 with: - # Because of https://github.com/tree-sitter/py-tree-sitter/issues/162 - python-version: '3.11.x' + python-version: "3.12" - name: Install CLI run: | pip install codecov-cli @@ -116,10 +115,9 @@ jobs: with: submodules: true fetch-depth: 0 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v5 with: - # Because of https://github.com/tree-sitter/py-tree-sitter/issues/162 - python-version: '3.11.x' + python-version: "3.12" - name: Install CLI run: | pip install -r requirements.txt -r tests/requirements.txt @@ -141,6 +139,7 @@ jobs: fail-fast: false matrix: include: + - python-version: "3.12" - python-version: "3.11" - python-version: "3.10" - python-version: "3.9" @@ -151,14 +150,14 @@ jobs: submodules: true fetch-depth: 2 - name: Set up Python ${{matrix.python-version}} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: "${{matrix.python-version}}" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - python setup.py develop + python -m pip install -e . pip install -r tests/requirements.txt - name: Test with pytest run: | diff --git a/codecov_cli/helpers/ci_adapters/codebuild.py b/codecov_cli/helpers/ci_adapters/codebuild.py index 056773c4..5b0b259c 100644 --- a/codecov_cli/helpers/ci_adapters/codebuild.py +++ b/codecov_cli/helpers/ci_adapters/codebuild.py @@ -12,7 +12,7 @@ def detect(self) -> bool: def _get_branch(self): branch = os.getenv("CODEBUILD_WEBHOOK_HEAD_REF") if branch: - return re.sub("^refs\/heads\/", "", branch) + return re.sub(r"^refs\/heads\/", "", branch) return None def _get_build_code(self): @@ -27,10 +27,10 @@ def _get_commit_sha(self): def _get_slug(self): slug = os.getenv("CODEBUILD_SOURCE_REPO_URL") if slug: - slug = re.sub("^.*github.com\/", "", slug) - slug = re.sub("^.*gitlab.com\/", "", slug) - slug = re.sub("^.*bitbucket.com\/", "", slug) - return re.sub("\.git$", "", slug) + slug = re.sub(r"^.*github.com\/", "", slug) + slug = re.sub(r"^.*gitlab.com\/", "", slug) + slug = re.sub(r"^.*bitbucket.com\/", "", slug) + return re.sub(r"\.git$", "", slug) return None def _get_service(self): @@ -39,7 +39,7 @@ def _get_service(self): def _get_pull_request_number(self): pr = os.getenv("CODEBUILD_SOURCE_VERSION") if pr and pr.startswith("pr/"): - return re.sub("^pr\/", "", pr) + return re.sub(r"^pr\/", "", pr) return None def _get_job_code(self): From 74c67b67568a4bafac886eb186dd50797c804f5e Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:01:23 -0400 Subject: [PATCH 078/128] feat: support new tokenless protocol (#447) * feat: add new tokenless support We no longer have to send the X-Tokenless header anymore. Signed-off-by: joseph-sentry * fix: remove github API call from tokenless we no longer want to make the github API call to check if the current ref we are uploading from is a PR to determine what to set the tokenless header to. It's no longer necessary and all that is needed for tokenless is to pass the correct branch name (containing a ':') to the create commit step so that tokenless works. The CLI will automatically check if the TOKENLESS env var has been set by the action. The action will set this env var when it detects it's running on a fork. Signed-off-by: joseph-sentry * fix: address feedback * fix: add clarifying comment Signed-off-by: joseph-sentry * test: fix tests * chore: make lint Signed-off-by: joseph-sentry * chore: make lint --------- Signed-off-by: joseph-sentry --- codecov_cli/helpers/git.py | 19 ----- codecov_cli/helpers/request.py | 19 ++++- codecov_cli/services/commit/__init__.py | 13 +-- codecov_cli/services/report/__init__.py | 13 +-- codecov_cli/services/upload/upload_sender.py | 17 +--- .../services/upload_completion/__init__.py | 4 +- tests/helpers/test_git.py | 79 ------------------- tests/helpers/test_request.py | 12 +++ tests/helpers/test_upload_sender.py | 11 +-- tests/services/commit/test_commit_service.py | 30 +------ tests/services/report/test_report_service.py | 37 --------- 11 files changed, 44 insertions(+), 210 deletions(-) diff --git a/codecov_cli/helpers/git.py b/codecov_cli/helpers/git.py index 89ae8d40..780cfd55 100644 --- a/codecov_cli/helpers/git.py +++ b/codecov_cli/helpers/git.py @@ -95,22 +95,3 @@ def parse_git_service(remote_repo_url: str): extra=dict(remote_repo_url=remote_repo_url), ) return None - - -def is_fork_pr(pull_dict: PullDict) -> bool: - """ - takes in dict: pull_dict - returns true if PR is made in a fork context, false if not. - """ - return pull_dict and pull_dict["head"]["slug"] != pull_dict["base"]["slug"] - - -def get_pull(service, slug, pr_num) -> Optional[PullDict]: - """ - takes in str git service e.g. github, gitlab etc., slug in the owner/repo format, and the pull request number - returns the pull request info gotten from the git service provider if successful, None if not - """ - git_service = get_git_service(service) - if git_service: - pull_dict = git_service.get_pull_request(slug, pr_num) - return pull_dict diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index 048efd23..a170b565 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -1,6 +1,7 @@ import logging from sys import exit from time import sleep +from typing import Optional import click import requests @@ -15,7 +16,7 @@ USER_AGENT = f"codecov-cli/{__version__}" -def _set_user_agent(headers: dict = None) -> dict: +def _set_user_agent(headers: Optional[dict] = None) -> dict: headers = headers or {} headers.setdefault("User-Agent", USER_AGENT) return headers @@ -37,7 +38,10 @@ def put(url: str, data: dict = None, headers: dict = None) -> requests.Response: def post( - url: str, data: dict = None, headers: dict = None, params: dict = None + url: str, + data: Optional[dict] = None, + headers: Optional[dict] = None, + params: Optional[dict] = None, ) -> requests.Response: headers = _set_user_agent(headers) return requests.post(url, json=data, headers=headers, params=params) @@ -82,7 +86,10 @@ def wrapper(*args, **kwargs): @retry_request def send_post_request( - url: str, data: dict = None, headers: dict = None, params: dict = None + url: str, + data: Optional[dict] = None, + headers: Optional[dict] = None, + params: Optional[dict] = None, ): return request_result(post(url=url, data=data, headers=headers, params=params)) @@ -95,6 +102,12 @@ def get_token_header_or_fail(token: str) -> dict: return {"Authorization": f"token {token}"} +def get_token_header(token: str) -> Optional[dict]: + if token is None: + return None + return {"Authorization": f"token {token}"} + + @retry_request def send_put_request( url: str, diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index 2c8cbdd4..a465c0df 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -1,9 +1,9 @@ import logging +import os import typing from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug -from codecov_cli.helpers.git import get_pull, is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -43,11 +43,12 @@ def create_commit_logic( def send_commit_data( commit_sha, parent_sha, pr, branch, slug, token, service, enterprise_url ): - decoded_slug = decode_slug(slug) - pull_dict = get_pull(service, decoded_slug, pr) if not token else None - if is_fork_pr(pull_dict): - headers = {"X-Tokenless": pull_dict["head"]["slug"], "X-Tokenless-PR": pr} - branch = pull_dict["head"]["slug"] + ":" + branch + # this is how the CLI receives the username of the user to whom the fork belongs + # to and the branch name from the action + tokenless = os.environ.get("TOKENLESS") + if tokenless: + headers = None # type: ignore + branch = tokenless # type: ignore logger.info("The PR is happening in a forked repo. Using tokenless upload.") else: headers = get_token_header_or_fail(token) diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 1b3cf16a..21b81ca4 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -7,7 +7,6 @@ from codecov_cli.helpers import request from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug -from codecov_cli.helpers.git import get_pull, is_fork_pr from codecov_cli.helpers.request import ( get_token_header_or_fail, log_warnings_and_errors_if_any, @@ -47,17 +46,7 @@ def send_create_report_request( commit_sha, code, service, token, encoded_slug, enterprise_url, pull_request_number ): data = {"code": code} - decoded_slug = decode_slug(encoded_slug) - pull_dict = ( - get_pull(service, decoded_slug, pull_request_number) if not token else None - ) - if is_fork_pr(pull_dict): - headers = { - "X-Tokenless": pull_dict["head"]["slug"], - "X-Tokenless-PR": pull_request_number, - } - else: - headers = get_token_header_or_fail(token) + headers = get_token_header_or_fail(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" return send_post_request(url=url, headers=headers, data=data) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index 3283f8e9..ada04bb3 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -8,9 +8,8 @@ from codecov_cli import __version__ as codecov_cli_version from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug -from codecov_cli.helpers.git import get_pull, is_fork_pr from codecov_cli.helpers.request import ( - get_token_header_or_fail, + get_token_header, send_post_request, send_put_request, ) @@ -53,19 +52,7 @@ def send_upload_data( "version": codecov_cli_version, "ci_service": ci_service, } - - # Data to upload to Codecov - pull_dict = ( - get_pull(git_service, slug, pull_request_number) if not token else None - ) - - if is_fork_pr(pull_dict): - headers = { - "X-Tokenless": pull_dict["head"]["slug"], - "X-Tokenless-PR": pull_request_number, - } - else: - headers = get_token_header_or_fail(token) + headers = get_token_header(token) encoded_slug = encode_slug(slug) upload_url = enterprise_url or CODECOV_API_URL url, data = self.get_url_and_possibly_update_data( diff --git a/codecov_cli/services/upload_completion/__init__.py b/codecov_cli/services/upload_completion/__init__.py index 9f7e2707..20f68872 100644 --- a/codecov_cli/services/upload_completion/__init__.py +++ b/codecov_cli/services/upload_completion/__init__.py @@ -4,7 +4,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.request import ( - get_token_header_or_fail, + get_token_header, log_warnings_and_errors_if_any, send_post_request, ) @@ -16,7 +16,7 @@ def upload_completion_logic( commit_sha, slug, token, git_service, enterprise_url, fail_on_error=False ): encoded_slug = encode_slug(slug) - headers = get_token_header_or_fail(token) + headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/upload-complete" sending_result = send_post_request(url=url, headers=headers) diff --git a/tests/helpers/test_git.py b/tests/helpers/test_git.py index f0415547..fdcc778e 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -135,82 +135,3 @@ def test_get_git_service_class(): assert git.get_git_service("bitbucket") == None -def test_pr_is_not_fork_pr(mocker): - def mock_request(*args, headers={}, **kwargs): - assert headers["X-GitHub-Api-Version"] == "2022-11-28" - res = { - "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", - "head": { - "sha": "123", - "label": "codecov-cli:branch", - "ref": "branch", - "repo": {"full_name": "codecov/codecov-cli"}, - }, - "base": { - "sha": "123", - "label": "codecov-cli:main", - "ref": "main", - "repo": {"full_name": "codecov/codecov-cli"}, - }, - } - response = Response() - response.status_code = 200 - response._content = json.dumps(res).encode("utf-8") - return response - - mocker.patch.object( - requests, - "get", - side_effect=mock_request, - ) - pull_dict = git.get_pull("github", "codecov/codecov-cli", 1) - assert not git.is_fork_pr(pull_dict) - - -def test_pr_is_fork_pr(mocker): - def mock_request(*args, headers={}, **kwargs): - assert headers["X-GitHub-Api-Version"] == "2022-11-28" - res = { - "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", - "head": { - "sha": "123", - "label": "codecov-cli:branch", - "ref": "branch", - "repo": {"full_name": "user_forked_repo/codecov-cli"}, - }, - "base": { - "sha": "123", - "label": "codecov-cli:main", - "ref": "main", - "repo": {"full_name": "codecov/codecov-cli"}, - }, - } - response = Response() - response.status_code = 200 - response._content = json.dumps(res).encode("utf-8") - return response - - mocker.patch.object( - requests, - "get", - side_effect=mock_request, - ) - pull_dict = git.get_pull("github", "codecov/codecov-cli", 1) - assert git.is_fork_pr(pull_dict) - - -def test_pr_not_found(mocker): - def mock_request(*args, headers={}, **kwargs): - assert headers["X-GitHub-Api-Version"] == "2022-11-28" - response = Response() - response.status_code = 404 - response._content = b"not-found" - return response - - mocker.patch.object( - requests, - "get", - side_effect=mock_request, - ) - pull_dict = git.get_pull("github", "codecov/codecov-cli", 1) - assert not git.is_fork_pr(pull_dict) diff --git a/tests/helpers/test_request.py b/tests/helpers/test_request.py index bbd6b531..8be7cef6 100644 --- a/tests/helpers/test_request.py +++ b/tests/helpers/test_request.py @@ -7,6 +7,7 @@ from codecov_cli import __version__ from codecov_cli.helpers.request import ( get, + get_token_header, get_token_header_or_fail, log_warnings_and_errors_if_any, ) @@ -69,6 +70,17 @@ def test_get_token_header_or_fail(): == "Codecov token not found. Please provide Codecov token with -t flag." ) +def test_get_token_header(): + # Test with a valid UUID token + token = uuid.uuid4() + result = get_token_header(token) + assert result == {"Authorization": f"token {str(token)}"} + + # Test with a None token + token = None + result = get_token_header(token) + assert result is None + def test_request_retry(mocker, valid_response): expected_response = request_result(valid_response) diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 19d6ddea..14195a43 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -231,14 +231,8 @@ def test_upload_sender_post_called_with_right_parameters_tokenless( mocked_storage_server, mocker, ): - headers = {"X-Tokenless": "user-forked/repo", "X-Tokenless-PR": "pr"} - mock_get_pull = mocker.patch( - "codecov_cli.services.upload.upload_sender.get_pull", - return_value={ - "head": {"slug": "user-forked/repo"}, - "base": {"slug": "org/repo"}, - }, - ) + headers = {} + mocked_legacy_upload_endpoint.match = [ matchers.json_params_matcher(request_data), matchers.header_matcher(headers), @@ -263,7 +257,6 @@ def test_upload_sender_post_called_with_right_parameters_tokenless( assert ( post_req_made.headers.items() >= headers.items() ) # test dict is a subset of the other - mock_get_pull.assert_called() def test_upload_sender_put_called_with_right_parameters( self, mocked_responses, mocked_legacy_upload_endpoint, mocked_storage_server diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 2b64e8ee..240040c2 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -150,33 +150,7 @@ def test_commit_sender_with_forked_repo(mocker): return_value=mocker.MagicMock(status_code=200, text="success"), ) - def mock_request(*args, headers={}, **kwargs): - assert headers["X-GitHub-Api-Version"] == "2022-11-28" - res = { - "url": "https://api.github.com/repos/codecov/codecov-cli/pulls/1", - "head": { - "sha": "123", - "label": "codecov-cli:branch", - "ref": "branch", - "repo": {"full_name": "user_forked_repo/codecov-cli"}, - }, - "base": { - "sha": "123", - "label": "codecov-cli:main", - "ref": "main", - "repo": {"full_name": "codecov/codecov-cli"}, - }, - } - response = Response() - response.status_code = 200 - response._content = json.dumps(res).encode("utf-8") - return response - - mocker.patch.object( - requests, - "get", - side_effect=mock_request, - ) + mocker.patch("os.environ", dict(TOKENLESS="user_forked_repo/codecov-cli:branch")) res = send_commit_data( "commit_sha", "parent_sha", @@ -195,5 +169,5 @@ def mock_request(*args, headers={}, **kwargs): "pullid": "1", "branch": "user_forked_repo/codecov-cli:branch", }, - headers={"X-Tokenless": "user_forked_repo/codecov-cli", "X-Tokenless-PR": "1"}, + headers=None, ) diff --git a/tests/services/report/test_report_service.py b/tests/services/report/test_report_service.py index e9987abe..8d31a112 100644 --- a/tests/services/report/test_report_service.py +++ b/tests/services/report/test_report_service.py @@ -26,43 +26,6 @@ def test_send_create_report_request_200(mocker): mocked_response.assert_called_once() -def test_send_create_report_request_200_tokneless(mocker): - mocked_response = mocker.patch( - "codecov_cli.services.report.send_post_request", - return_value=RequestResult( - status_code=200, - error=None, - warnings=[], - text="mocked response", - ), - ) - - mocked_get_pull = mocker.patch( - "codecov_cli.services.report.get_pull", - return_value={ - "head": {"slug": "user-forked/repo"}, - "base": {"slug": "org/repo"}, - }, - ) - res = send_create_report_request( - "commit_sha", - "code", - "github", - None, - "owner::::repo", - "enterprise_url", - 1, - ) - assert res.error is None - assert res.warnings == [] - mocked_response.assert_called_with( - url=f"enterprise_url/upload/github/owner::::repo/commits/commit_sha/reports", - headers={"X-Tokenless": "user-forked/repo", "X-Tokenless-PR": 1}, - data={"code": "code"}, - ) - mocked_get_pull.assert_called() - - def test_send_create_report_request_403(mocker): mocked_response = mocker.patch( "codecov_cli.services.report.requests.post", From 08c5a7da7c74e635b6db09d89804037a5abdd428 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 20 Jun 2024 16:42:02 -0400 Subject: [PATCH 079/128] Prepare release 0.7.0 (#459) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de949752..28dbcb1d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.6.0", + version="0.7.0", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 084851e0f99966bb319fa8ad59cf2527aa71f887 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:52:12 +0800 Subject: [PATCH 080/128] fix: remove token check for report (#461) * fix: remove token check for report * fix: use the entire blip * fix: don't try tokenless in create report results Signed-off-by: joseph-sentry --------- Signed-off-by: joseph-sentry Co-authored-by: joseph-sentry --- codecov_cli/services/report/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 21b81ca4..b8e7c445 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -8,6 +8,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug from codecov_cli.helpers.request import ( + get_token_header, get_token_header_or_fail, log_warnings_and_errors_if_any, request_result, @@ -46,7 +47,7 @@ def send_create_report_request( commit_sha, code, service, token, encoded_slug, enterprise_url, pull_request_number ): data = {"code": code} - headers = get_token_header_or_fail(token) + headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" return send_post_request(url=url, headers=headers, data=data) From 65a32bcf1a4177daccc2058d36cacfd2ff02c620 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Fri, 21 Jun 2024 12:12:12 -0400 Subject: [PATCH 081/128] Prepare release 0.7.1 (#462) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 28dbcb1d..c8508b60 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.0", + version="0.7.1", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 992826d53a50640c2c22b543b788921dd58aa0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Wed, 26 Jun 2024 22:08:57 +0200 Subject: [PATCH 082/128] =?UTF-8?q?=F0=9F=90=9B=20Use=20config-set=20token?= =?UTF-8?q?=20as=20CLI=20default=20fallback=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just like in the old days... Before this patch, the v4-related rewrite of the Codecov uploader CLI lost the ability to use the token set in the config file [[1]]. This change fixes the regression recovering the original behavior. The bug has been discovered during the re-configuration of ``codecov/codecov-action`` in ``pytest-dev/pytest``. We use the clear-text token to improve stability of uploads in PRs from forks so it makes sense to keep it in the config rather than in the GitHub Actions CI/CD definition file. [1]: https://docs.codecov.com/docs/codecovyml-reference#codecovtoken --- codecov_cli/main.py | 2 + tests/commands/test_upload_token_discovery.py | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tests/commands/test_upload_token_discovery.py diff --git a/codecov_cli/main.py b/codecov_cli/main.py index bd55d071..e1a50728 100644 --- a/codecov_cli/main.py +++ b/codecov_cli/main.py @@ -58,6 +58,8 @@ def cli( ctx.obj["codecov_yaml"] = load_cli_config(codecov_yml_path) if ctx.obj["codecov_yaml"] is None: logger.debug("No codecov_yaml found") + elif (token := ctx.obj["codecov_yaml"].get("codecov", {}).get("token")) is not None: + ctx.default_map = {ctx.invoked_subcommand: {"token": token}} ctx.obj["enterprise_url"] = enterprise_url diff --git a/tests/commands/test_upload_token_discovery.py b/tests/commands/test_upload_token_discovery.py new file mode 100644 index 00000000..5080d567 --- /dev/null +++ b/tests/commands/test_upload_token_discovery.py @@ -0,0 +1,46 @@ +"""Tests ensuring that an env-provided token can be found.""" + +from pathlib import Path +from textwrap import dedent as _dedent_text_block + +from click.testing import CliRunner +from pytest import MonkeyPatch +from pytest_mock import MockerFixture + +from codecov_cli.commands import upload +from codecov_cli.main import cli + + +def test_no_cli_token_config_fallback( + mocker: MockerFixture, + monkeypatch: MonkeyPatch, + tmp_path: Path, +) -> None: + """Test that a config-stored token is used with no CLI argument.""" + # NOTE: The pytest's `caplog` fixture is not used in this test as it + # NOTE: doesn't play well with Click's testing CLI runner, and does + # NOTE: not capture any log entries for mysterious reasons. + # + # Refs: + # * https://github.com/pallets/click/issues/2573#issuecomment-1649773563 + # * https://github.com/pallets/click/issues/1763#issuecomment-767687608 + (tmp_path / ".codecov.yml").write_text( + _dedent_text_block( + """ + --- + + codecov: + token: sentinel-value + + ... + """ + ) + ) + monkeypatch.chdir(tmp_path) + + mocker.patch.object(upload, "do_upload_logic") + do_upload_cmd_spy = mocker.spy(upload, "do_upload_logic") + + CliRunner().invoke(cli, ["do-upload", "--commit-sha=deadbeef"], obj={}) + + assert do_upload_cmd_spy.call_args[-1]["token"] == "sentinel-value" From a17031afb52204371015dfc53a3cd50de97b7c5e Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Fri, 28 Jun 2024 11:15:13 -0400 Subject: [PATCH 083/128] Prepare release 0.7.2 (#467) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c8508b60..f0dc51d1 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.1", + version="0.7.2", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 94215aa8b62ece2bd31ba0556452c48fa332b972 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:57:30 -0400 Subject: [PATCH 084/128] fix: add branch to test results upload (#471) --- codecov_cli/services/upload/upload_sender.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index ada04bb3..4b9817d2 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -60,6 +60,7 @@ def send_upload_data( upload_file_type, upload_url, git_service, + branch, encoded_slug, commit_sha, report_code, @@ -169,6 +170,7 @@ def get_url_and_possibly_update_data( report_type, upload_url, git_service, + branch, encoded_slug, commit_sha, report_code, @@ -177,6 +179,7 @@ def get_url_and_possibly_update_data( url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/uploads" elif report_type == "test_results": data["slug"] = encoded_slug + data["branch"] = branch data["commit"] = commit_sha data["service"] = git_service url = f"{upload_url}/upload/test_results/v1" From fc8fc47484e090b497cce9d5ba907b45e46aa09f Mon Sep 17 00:00:00 2001 From: Joe Becher Date: Mon, 15 Jul 2024 13:20:09 -0400 Subject: [PATCH 085/128] fix: exclude yaml files (#472) --- codecov_cli/services/upload/file_finder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index 79e845e3..f641f039 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -111,6 +111,8 @@ "*.whl", "*.xcconfig", "*.xcoverage.*", + "*.yml", + "*.yaml", "*/classycle/report.xml", "*codecov.yml", "*~", From fd371506525c7189f5bb5c6b66ae2f90e36a665e Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Mon, 15 Jul 2024 13:01:22 -0500 Subject: [PATCH 086/128] fix: use azure pipelines PR commit ID (#474) In the case of pull requests, Build.SourceVersion contains the commit ID of the merge commit. System.PullRequest.SourceCommitId contains the source commit ID of the pull request, so it should be checked first. --- codecov_cli/helpers/ci_adapters/azure_pipelines.py | 4 +++- tests/ci_adapters/test_azure_pipelines.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/codecov_cli/helpers/ci_adapters/azure_pipelines.py b/codecov_cli/helpers/ci_adapters/azure_pipelines.py index f444ecb1..4e253068 100644 --- a/codecov_cli/helpers/ci_adapters/azure_pipelines.py +++ b/codecov_cli/helpers/ci_adapters/azure_pipelines.py @@ -10,7 +10,9 @@ def detect(self) -> bool: return bool(os.getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI")) def _get_commit_sha(self): - return os.getenv("BUILD_SOURCEVERSION") + return os.getenv("SYSTEM_PULLREQUEST_SOURCECOMMITID") or os.getenv( + "BUILD_SOURCEVERSION" + ) def _get_build_url(self): if os.getenv("SYSTEM_TEAMPROJECT") and os.getenv("BUILD_BUILDID"): diff --git a/tests/ci_adapters/test_azure_pipelines.py b/tests/ci_adapters/test_azure_pipelines.py index fe06cbc3..5dd71d0f 100644 --- a/tests/ci_adapters/test_azure_pipelines.py +++ b/tests/ci_adapters/test_azure_pipelines.py @@ -14,6 +14,7 @@ class AzurePipelinesEnvEnum(str, Enum): BUILD_SOURCEVERSION = "BUILD_SOURCEVERSION" SYSTEM_PULLREQUEST_PULLREQUESTID = "SYSTEM_PULLREQUEST_PULLREQUESTID" SYSTEM_PULLREQUEST_PULLREQUESTNUMBER = "SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" + SYSTEM_PULLREQUEST_SOURCECOMMITID = "SYSTEM_PULLREQUEST_SOURCECOMMITID" SYSTEM_TEAMFOUNDATIONCOLLECTIONURI = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" SYSTEM_TEAMPROJECT = "SYSTEM_TEAMPROJECT" BUILD_REPOSITORY_NAME = "BUILD_REPOSITORY_NAME" @@ -43,6 +44,13 @@ def test_detect(self, env_dict, expected, mocker): {AzurePipelinesEnvEnum.BUILD_SOURCEVERSION: "123456789000111"}, "123456789000111", ), + ( + { + AzurePipelinesEnvEnum.BUILD_SOURCEVERSION: "123456789000111", + AzurePipelinesEnvEnum.SYSTEM_PULLREQUEST_SOURCECOMMITID: "111000987654321" + }, + "111000987654321", + ), ], ) def test_commit_sha(self, env_dict, expected, mocker): From f509528a3400ef46ee44f0027cd69a0f8a0ad8da Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:27:09 +1000 Subject: [PATCH 087/128] chore: consistent naming of upload options (#473) * chore: consistent naming of upload options Makes the `--report-code` of `do-upload` to also accept `--code`. This makes it consistent with other commands. closes codecov/feedback#380 * attempt to fix build that is broken for some reason --- .github/workflows/build_assets.yml | 6 ++---- codecov_cli/commands/upload.py | 2 ++ tests/commands/test_invoke_upload_process.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_assets.yml b/.github/workflows/build_assets.yml index e541c5f3..1f2b0110 100644 --- a/.github/workflows/build_assets.yml +++ b/.github/workflows/build_assets.yml @@ -31,8 +31,7 @@ jobs: - os: ubuntu-20.04 TARGET: ubuntu CMD_REQS: > - pip install -r requirements.txt - pip install . + pip install -r requirements.txt && pip install . CMD_BUILD: > STATICCODECOV_LIB_PATH=$(find build/ -maxdepth 1 -type d -name 'lib.*' -print -quit | xargs -I {} sh -c "find {} -type f -name 'staticcodecov*' -print -quit | sed 's|^./||'") && pyinstaller --add-binary ${STATICCODECOV_LIB_PATH}:. --copy-metadata codecov-cli --hidden-import staticcodecov_languages -F codecov_cli/main.py && @@ -42,8 +41,7 @@ jobs: - os: windows-latest TARGET: windows CMD_REQS: > - pip install -r requirements.txt - pip install . + pip install -r requirements.txt && pip install . CMD_BUILD: > pyinstaller --add-binary "build\lib.win-amd64-cpython-311\staticcodecov_languages.cp311-win_amd64.pyd;." --copy-metadata codecov-cli --hidden-import staticcodecov_languages -F codecov_cli\main.py && Copy-Item -Path ".\dist\main.exe" -Destination ".\dist\codecovcli_windows.exe" diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index 377553c2..3d6af124 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -18,7 +18,9 @@ def _turn_env_vars_into_dict(ctx, params, value): _global_upload_options = [ click.option( + "--code", "--report-code", + "report_code", help="The code of the report. If unsure, leave default", default="default", ), diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 10d2a52d..ec3981b2 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -73,7 +73,7 @@ def test_upload_process_options(mocker): " -t, --token TEXT Codecov upload token", " -r, --slug TEXT owner/repo slug used instead of the private", " repo token in Self-hosted", - " --report-code TEXT The code of the report. If unsure, leave", + " --code, --report-code TEXT The code of the report. If unsure, leave", " default", " --network-root-folder PATH Root folder from which to consider paths on", " the network section [default: (Current", From 33d25314fcc8aeef6887dffb5040daa5af4bf377 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Mon, 22 Jul 2024 14:57:13 -0400 Subject: [PATCH 088/128] Prepare release 0.7.3 (#476) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f0dc51d1..42988d55 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.2", + version="0.7.3", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 8810ef7fc2bb5662eb53a0582fc41f6e8b296508 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 2 Aug 2024 09:20:32 -0400 Subject: [PATCH 089/128] feat: disable search means don't search for explicitly mentioned files (#479) Signed-off-by: joseph-sentry --- codecov_cli/helpers/folder_searcher.py | 2 +- codecov_cli/services/upload/file_finder.py | 9 +++++++++ tests/commands/test_process_test_results.py | 9 ++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/codecov_cli/helpers/folder_searcher.py b/codecov_cli/helpers/folder_searcher.py index 19e1be46..dc28a572 100644 --- a/codecov_cli/helpers/folder_searcher.py +++ b/codecov_cli/helpers/folder_searcher.py @@ -37,7 +37,7 @@ def search_files( filename_exclude_regex: typing.Optional[typing.Pattern] = None, multipart_include_regex: typing.Optional[typing.Pattern] = None, multipart_exclude_regex: typing.Optional[typing.Pattern] = None, - search_for_directories: bool = False + search_for_directories: bool = False, ) -> typing.Generator[pathlib.Path, None, None]: """ " Searches for files or directories in a given folder diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index f641f039..428beb58 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -226,8 +226,17 @@ def find_files(self) -> typing.List[UploadCollectionResultFile]: return list(set(result_files + user_result_files)) def get_user_specified_files(self, regex_patterns_to_exclude): + if self.disable_search: + result = [] + for file in self.explicitly_listed_files: + filepath = Path(file) + if filepath.exists(): + result.append(filepath) + return result + user_filenames_to_include = [] files_excluded_but_user_includes = [] + for file in self.explicitly_listed_files: user_filenames_to_include.append(file.name) if regex_patterns_to_exclude.match(file.name): diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index d0c62eb7..df3bf0ae 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -1,4 +1,3 @@ -import logging import os from click.testing import CliRunner @@ -11,7 +10,6 @@ def test_process_test_results( mocker, tmpdir, ): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( @@ -44,7 +42,6 @@ def test_process_test_results( assert result.exit_code == 0 - mocked_post.assert_called_with( url="https://api.github.com/repos/fake/repo/issues/pull/comments", data={ @@ -58,7 +55,6 @@ def test_process_test_results( ) - def test_process_test_results_non_existent_file(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") @@ -93,7 +89,7 @@ def test_process_test_results_non_existent_file(mocker, tmpdir): assert result.exit_code == 1 expected_logs = [ "ci service found", - 'Some files were not found', + "No JUnit XML files were found", ] for log in expected_logs: assert log in result.output @@ -182,7 +178,6 @@ def test_process_test_results_missing_ref(mocker, tmpdir): assert log in result.output - def test_process_test_results_missing_step_summary(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") @@ -221,4 +216,4 @@ def test_process_test_results_missing_step_summary(mocker, tmpdir): "Error: Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable.", ] for log in expected_logs: - assert log in result.output \ No newline at end of file + assert log in result.output From 1478514b1203d1c3921ca60b767c4224d0c417e4 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:05:41 -0400 Subject: [PATCH 090/128] =?UTF-8?q?Revert=20"feat:=20disable=20search=20me?= =?UTF-8?q?ans=20don't=20search=20for=20explicitly=20mentioned=20file?= =?UTF-8?q?=E2=80=A6"=20(#480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8810ef7fc2bb5662eb53a0582fc41f6e8b296508. --- codecov_cli/helpers/folder_searcher.py | 2 +- codecov_cli/services/upload/file_finder.py | 9 --------- tests/commands/test_process_test_results.py | 9 +++++++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/codecov_cli/helpers/folder_searcher.py b/codecov_cli/helpers/folder_searcher.py index dc28a572..19e1be46 100644 --- a/codecov_cli/helpers/folder_searcher.py +++ b/codecov_cli/helpers/folder_searcher.py @@ -37,7 +37,7 @@ def search_files( filename_exclude_regex: typing.Optional[typing.Pattern] = None, multipart_include_regex: typing.Optional[typing.Pattern] = None, multipart_exclude_regex: typing.Optional[typing.Pattern] = None, - search_for_directories: bool = False, + search_for_directories: bool = False ) -> typing.Generator[pathlib.Path, None, None]: """ " Searches for files or directories in a given folder diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index 428beb58..f641f039 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -226,17 +226,8 @@ def find_files(self) -> typing.List[UploadCollectionResultFile]: return list(set(result_files + user_result_files)) def get_user_specified_files(self, regex_patterns_to_exclude): - if self.disable_search: - result = [] - for file in self.explicitly_listed_files: - filepath = Path(file) - if filepath.exists(): - result.append(filepath) - return result - user_filenames_to_include = [] files_excluded_but_user_includes = [] - for file in self.explicitly_listed_files: user_filenames_to_include.append(file.name) if regex_patterns_to_exclude.match(file.name): diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index df3bf0ae..d0c62eb7 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -1,3 +1,4 @@ +import logging import os from click.testing import CliRunner @@ -10,6 +11,7 @@ def test_process_test_results( mocker, tmpdir, ): + tmp_file = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( @@ -42,6 +44,7 @@ def test_process_test_results( assert result.exit_code == 0 + mocked_post.assert_called_with( url="https://api.github.com/repos/fake/repo/issues/pull/comments", data={ @@ -55,6 +58,7 @@ def test_process_test_results( ) + def test_process_test_results_non_existent_file(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") @@ -89,7 +93,7 @@ def test_process_test_results_non_existent_file(mocker, tmpdir): assert result.exit_code == 1 expected_logs = [ "ci service found", - "No JUnit XML files were found", + 'Some files were not found', ] for log in expected_logs: assert log in result.output @@ -178,6 +182,7 @@ def test_process_test_results_missing_ref(mocker, tmpdir): assert log in result.output + def test_process_test_results_missing_step_summary(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") @@ -216,4 +221,4 @@ def test_process_test_results_missing_step_summary(mocker, tmpdir): "Error: Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable.", ] for log in expected_logs: - assert log in result.output + assert log in result.output \ No newline at end of file From fdcfd3793eca075455f3d64c69ac42e23a7728a2 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:41:00 -0400 Subject: [PATCH 091/128] build: use buster debian image for arm image (#483) --- .github/workflows/build_assets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_assets.yml b/.github/workflows/build_assets.yml index 1f2b0110..b12941e6 100644 --- a/.github/workflows/build_assets.yml +++ b/.github/workflows/build_assets.yml @@ -91,7 +91,7 @@ jobs: - distro: "python:3.11-alpine3.18" arch: x86_64 distro_name: alpine - - distro: "python:3.11" + - distro: "python:3.11-bullseye" arch: arm64 distro_name: linux From ec878b09dc6fa3c74598a5622d2705102ccd741f Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 15 Aug 2024 19:17:18 -0400 Subject: [PATCH 092/128] Prepare release 0.7.4 (#484) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42988d55..bb92b319 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.3", + version="0.7.4", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From ae3fc7eb5783e3fb218f32d8138515cbf94f5c9d Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:54:24 +0300 Subject: [PATCH 093/128] fix: sanitize yaml token from logs (#485) * fix: sanitize yaml token from logs * fix: take into account JSON decoding error * fix: add unit tests --- codecov_cli/helpers/request.py | 29 ++++++++++++++++++++++++++++- tests/helpers/test_request.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index a170b565..dbafc9a3 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -1,3 +1,4 @@ +import json import logging from sys import exit from time import sleep @@ -143,7 +144,9 @@ def log_warnings_and_errors_if_any( ) logger.debug( f"{process_desc} result", - extra=dict(extra_log_attributes=dict(result=sending_result)), + extra=dict( + extra_log_attributes=dict(result=_sanitize_request_result(sending_result)) + ), ) if sending_result.warnings: number_warnings = len(sending_result.warnings) @@ -157,3 +160,27 @@ def log_warnings_and_errors_if_any( logger.error(f"{process_desc} failed: {sending_result.error.description}") if fail_on_error: exit(1) + + +def _sanitize_request_result(result: RequestResult): + if not hasattr(result, "text"): + return result + + try: + text_as_dict = json.loads(result.text) + token = text_as_dict.get("repository").get("yaml").get("codecov").get("token") + if token: + sanitized_token = str(token)[:1] + 18 * "*" + text_as_dict["repository"]["yaml"]["codecov"]["token"] = sanitized_token + sanitized_text = json.dumps(text_as_dict) + + return RequestResult( + status_code=result.status_code, + error=result.error, + warnings=result.warnings, + text=sanitized_text, + ) + except (AttributeError, json.JSONDecodeError): + pass + + return result diff --git a/tests/helpers/test_request.py b/tests/helpers/test_request.py index 8be7cef6..1125f93f 100644 --- a/tests/helpers/test_request.py +++ b/tests/helpers/test_request.py @@ -54,6 +54,38 @@ def test_log_error_raise(mocker): mock_log_error.assert_called_with(f"Process failed: Unauthorized") +def test_log_result_without_token(mocker): + mock_log_debug = mocker.patch.object(req_log, "debug") + result = RequestResult( + error=None, + warnings=[], + status_code=201, + text="{\"message\":\"commit\",\"timestamp\":\"2024-03-25T15:41:07Z\",\"ci_passed\":true,\"state\":\"complete\",\"repository\":{\"name\":\"repo\",\"is_private\":false,\"active\":true,\"language\":\"python\",\"yaml\":null},\"author\":{\"avatar_url\":\"https://example.com\",\"service\":\"github\",\"username\":null,\"name\":\"dependabot[bot]\",\"ownerid\":2780265},\"commitid\":\"commit\",\"parent_commit_id\":\"parent\",\"pullid\":1,\"branch\":\"main\"}" + ) + log_warnings_and_errors_if_any(result, "Commit creating", False) + mock_log_debug.assert_called_with('Commit creating result', extra={'extra_log_attributes': {'result': result}}) + + +def test_log_result_with_token(mocker): + mock_log_debug = mocker.patch.object(req_log, "debug") + result = RequestResult( + error=None, + warnings=[], + status_code=201, + text="{\"message\": \"commit\", \"timestamp\": \"2024-07-16T20:51:07Z\", \"ci_passed\": true, \"state\": \"complete\", \"repository\": {\"name\": \"repo\", \"is_private\": false, \"active\": true, \"language\": \"python\", \"yaml\": {\"codecov\": {\"token\": \"faketoken\"}}, \"author\": {\"avatar_url\": \"https://example.com\", \"service\": \"github\", \"username\": \"author\", \"name\": \"author\", \"ownerid\": 3461769}, \"commitid\": \"commit\", \"parent_commit_id\": \"parent_commit\", \"pullid\": null, \"branch\": \"main\"}}" + ) + + expected_text = "{\"message\": \"commit\", \"timestamp\": \"2024-07-16T20:51:07Z\", \"ci_passed\": true, \"state\": \"complete\", \"repository\": {\"name\": \"repo\", \"is_private\": false, \"active\": true, \"language\": \"python\", \"yaml\": {\"codecov\": {\"token\": \"f******************\"}}, \"author\": {\"avatar_url\": \"https://example.com\", \"service\": \"github\", \"username\": \"author\", \"name\": \"author\", \"ownerid\": 3461769}, \"commitid\": \"commit\", \"parent_commit_id\": \"parent_commit\", \"pullid\": null, \"branch\": \"main\"}}" + expected = RequestResult( + error=None, + warnings=[], + status_code=201, + text=expected_text, + ) + log_warnings_and_errors_if_any(result, "Commit creating", False) + mock_log_debug.assert_called_with('Commit creating result', extra={'extra_log_attributes': {'result': expected}}) + + def test_get_token_header_or_fail(): # Test with a valid UUID token token = uuid.uuid4() From 6c2f585ead7d5b11d28c604c50fea8348a8351bd Mon Sep 17 00:00:00 2001 From: Giovanni M Guidini <99758426+giovanni-guidini@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:45:46 -0300 Subject: [PATCH 094/128] chore: typehint context object (#496) --- codecov_cli/commands/base_picking.py | 3 +- codecov_cli/commands/commit.py | 3 +- codecov_cli/commands/create_report_result.py | 3 +- codecov_cli/commands/empty_upload.py | 3 +- codecov_cli/commands/get_report_results.py | 3 +- codecov_cli/commands/labelanalysis.py | 3 +- codecov_cli/commands/report.py | 3 +- codecov_cli/commands/send_notifications.py | 3 +- codecov_cli/commands/staticanalysis.py | 3 +- codecov_cli/commands/upload.py | 3 +- codecov_cli/commands/upload_process.py | 3 +- codecov_cli/helpers/ci_adapters/__init__.py | 4 ++- codecov_cli/helpers/config.py | 4 +-- codecov_cli/helpers/folder_searcher.py | 2 +- codecov_cli/helpers/versioning_systems.py | 18 ++++------ codecov_cli/types.py | 36 ++++++++++++++------ 16 files changed, 60 insertions(+), 37 deletions(-) diff --git a/codecov_cli/commands/base_picking.py b/codecov_cli/commands/base_picking.py index 16d70bd9..372bc888 100644 --- a/codecov_cli/commands/base_picking.py +++ b/codecov_cli/commands/base_picking.py @@ -6,6 +6,7 @@ from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum from codecov_cli.helpers.encoder import slug_without_subgroups_is_invalid from codecov_cli.services.commit.base_picking import base_picking_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -45,7 +46,7 @@ ) @click.pass_context def pr_base_picking( - ctx, + ctx: CommandContext, base_sha: str, pr: typing.Optional[int], slug: typing.Optional[str], diff --git a/codecov_cli/commands/commit.py b/codecov_cli/commands/commit.py index 15a879d4..316fbdff 100644 --- a/codecov_cli/commands/commit.py +++ b/codecov_cli/commands/commit.py @@ -7,6 +7,7 @@ from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.commit import create_commit_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -35,7 +36,7 @@ @global_options @click.pass_context def create_commit( - ctx, + ctx: CommandContext, commit_sha: str, parent_sha: typing.Optional[str], pull_request_number: typing.Optional[int], diff --git a/codecov_cli/commands/create_report_result.py b/codecov_cli/commands/create_report_result.py index 1aa1c21f..27dae8da 100644 --- a/codecov_cli/commands/create_report_result.py +++ b/codecov_cli/commands/create_report_result.py @@ -4,6 +4,7 @@ from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_results_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -15,7 +16,7 @@ @global_options @click.pass_context def create_report_results( - ctx, + ctx: CommandContext, commit_sha: str, code: str, slug: str, diff --git a/codecov_cli/commands/empty_upload.py b/codecov_cli/commands/empty_upload.py index 8cabec8b..7af5a246 100644 --- a/codecov_cli/commands/empty_upload.py +++ b/codecov_cli/commands/empty_upload.py @@ -7,6 +7,7 @@ from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.empty_upload import empty_upload_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -16,7 +17,7 @@ @global_options @click.pass_context def empty_upload( - ctx, + ctx: CommandContext, commit_sha: str, force: bool, slug: typing.Optional[str], diff --git a/codecov_cli/commands/get_report_results.py b/codecov_cli/commands/get_report_results.py index a10ccefa..6487dd0c 100644 --- a/codecov_cli/commands/get_report_results.py +++ b/codecov_cli/commands/get_report_results.py @@ -7,6 +7,7 @@ from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.report import send_reports_result_get_request +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -18,7 +19,7 @@ @global_options @click.pass_context def get_report_results( - ctx, + ctx: CommandContext, commit_sha: str, code: str, slug: str, diff --git a/codecov_cli/commands/labelanalysis.py b/codecov_cli/commands/labelanalysis.py index a2eda197..ec1cfb24 100644 --- a/codecov_cli/commands/labelanalysis.py +++ b/codecov_cli/commands/labelanalysis.py @@ -16,6 +16,7 @@ LabelAnalysisRequestResult, LabelAnalysisRunnerInterface, ) +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -77,7 +78,7 @@ ) @click.pass_context def label_analysis( - ctx: click.Context, + ctx: CommandContext, token: str, head_commit_sha: str, base_commit_sha: str, diff --git a/codecov_cli/commands/report.py b/codecov_cli/commands/report.py index 1e57bb29..25d63194 100644 --- a/codecov_cli/commands/report.py +++ b/codecov_cli/commands/report.py @@ -5,6 +5,7 @@ from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -25,7 +26,7 @@ @global_options @click.pass_context def create_report( - ctx, + ctx: CommandContext, commit_sha: str, code: str, slug: str, diff --git a/codecov_cli/commands/send_notifications.py b/codecov_cli/commands/send_notifications.py index 779e4054..7798a6e3 100644 --- a/codecov_cli/commands/send_notifications.py +++ b/codecov_cli/commands/send_notifications.py @@ -7,6 +7,7 @@ from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.upload_completion import upload_completion_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -15,7 +16,7 @@ @global_options @click.pass_context def send_notifications( - ctx, + ctx: CommandContext, commit_sha: str, slug: typing.Optional[str], token: typing.Optional[str], diff --git a/codecov_cli/commands/staticanalysis.py b/codecov_cli/commands/staticanalysis.py index 62bb65e3..265efcc9 100644 --- a/codecov_cli/commands/staticanalysis.py +++ b/codecov_cli/commands/staticanalysis.py @@ -8,6 +8,7 @@ from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum from codecov_cli.helpers.validators import validate_commit_sha from codecov_cli.services.staticanalysis import run_analysis_entrypoint +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -48,7 +49,7 @@ ) @click.pass_context def static_analysis( - ctx, + ctx: CommandContext, foldertosearch, numberprocesses, pattern, diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index 3d6af124..c14f5614 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -8,6 +8,7 @@ from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum from codecov_cli.helpers.options import global_options from codecov_cli.services.upload import do_upload_logic +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -190,7 +191,7 @@ def global_upload_options(func): @global_options @click.pass_context def do_upload( - ctx: click.Context, + ctx: CommandContext, commit_sha: str, report_code: str, branch: typing.Optional[str], diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index 10fa5847..2d12a587 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -8,6 +8,7 @@ from codecov_cli.commands.report import create_report from codecov_cli.commands.upload import do_upload, global_upload_options from codecov_cli.helpers.options import global_options +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -22,7 +23,7 @@ ) @click.pass_context def upload_process( - ctx, + ctx: CommandContext, commit_sha: str, report_code: str, build_code: typing.Optional[str], diff --git a/codecov_cli/helpers/ci_adapters/__init__.py b/codecov_cli/helpers/ci_adapters/__init__.py index 26215d86..fa142239 100644 --- a/codecov_cli/helpers/ci_adapters/__init__.py +++ b/codecov_cli/helpers/ci_adapters/__init__.py @@ -1,7 +1,9 @@ import logging +from typing import Optional from codecov_cli.helpers.ci_adapters.appveyor_ci import AppveyorCIAdapter from codecov_cli.helpers.ci_adapters.azure_pipelines import AzurePipelinesCIAdapter +from codecov_cli.helpers.ci_adapters.base import CIAdapterBase from codecov_cli.helpers.ci_adapters.bitbucket_ci import BitbucketAdapter from codecov_cli.helpers.ci_adapters.bitrise_ci import BitriseCIAdapter from codecov_cli.helpers.ci_adapters.buildkite import BuildkiteAdapter @@ -22,7 +24,7 @@ logger = logging.getLogger("codecovcli") -def get_ci_adapter(provider_name: str = None): +def get_ci_adapter(provider_name: str = None) -> Optional[CIAdapterBase]: if provider_name: for provider in get_ci_providers_list(): if provider.get_service_name().lower() == provider_name.lower(): diff --git a/codecov_cli/helpers/config.py b/codecov_cli/helpers/config.py index e8941056..870ac525 100644 --- a/codecov_cli/helpers/config.py +++ b/codecov_cli/helpers/config.py @@ -1,6 +1,6 @@ import logging import pathlib -import typing +import typing as t import yaml @@ -45,7 +45,7 @@ def _find_codecov_yamls(): return yamls -def load_cli_config(codecov_yml_path: typing.Optional[pathlib.Path]): +def load_cli_config(codecov_yml_path: t.Optional[pathlib.Path]) -> t.Optional[dict]: if not codecov_yml_path: yamls = _find_codecov_yamls() codecov_yml_path = yamls[0] if yamls else None diff --git a/codecov_cli/helpers/folder_searcher.py b/codecov_cli/helpers/folder_searcher.py index 19e1be46..dc28a572 100644 --- a/codecov_cli/helpers/folder_searcher.py +++ b/codecov_cli/helpers/folder_searcher.py @@ -37,7 +37,7 @@ def search_files( filename_exclude_regex: typing.Optional[typing.Pattern] = None, multipart_include_regex: typing.Optional[typing.Pattern] = None, multipart_exclude_regex: typing.Optional[typing.Pattern] = None, - search_for_directories: bool = False + search_for_directories: bool = False, ) -> typing.Generator[pathlib.Path, None, None]: """ " Searches for files or directories in a given folder diff --git a/codecov_cli/helpers/versioning_systems.py b/codecov_cli/helpers/versioning_systems.py index b6a53df2..143f8c88 100644 --- a/codecov_cli/helpers/versioning_systems.py +++ b/codecov_cli/helpers/versioning_systems.py @@ -1,6 +1,6 @@ import logging import subprocess -import typing +import typing as t from pathlib import Path from shutil import which @@ -14,21 +14,17 @@ class VersioningSystemInterface(object): def __repr__(self) -> str: return str(type(self)) - def get_fallback_value( - self, fallback_field: FallbackFieldEnum - ) -> typing.Optional[str]: + def get_fallback_value(self, fallback_field: FallbackFieldEnum) -> t.Optional[str]: pass - def get_network_root(self) -> typing.Optional[Path]: + def get_network_root(self) -> t.Optional[Path]: pass - def list_relevant_files( - self, directory: typing.Optional[Path] = None - ) -> typing.List[str]: + def list_relevant_files(self, directory: t.Optional[Path] = None) -> t.List[str]: pass -def get_versioning_system() -> VersioningSystemInterface: +def get_versioning_system() -> t.Optional[VersioningSystemInterface]: for klass in [GitVersioningSystem, NoVersioningSystem]: if klass.is_available(): logger.debug(f"versioning system found: {klass}") @@ -123,9 +119,7 @@ def get_network_root(self): return Path(p.stdout.decode().rstrip()) return None - def list_relevant_files( - self, root_folder: typing.Optional[Path] = None - ) -> typing.List[str]: + def list_relevant_files(self, root_folder: t.Optional[Path] = None) -> t.List[str]: dir_to_use = root_folder or self.get_network_root() if dir_to_use is None: raise ValueError("Can't determine root folder") diff --git a/codecov_cli/types.py b/codecov_cli/types.py index d9405f2e..4050c47e 100644 --- a/codecov_cli/types.py +++ b/codecov_cli/types.py @@ -1,7 +1,23 @@ import pathlib -import typing +import typing as t from dataclasses import dataclass +import click + +from codecov_cli.helpers.ci_adapters.base import CIAdapterBase +from codecov_cli.helpers.versioning_systems import VersioningSystemInterface + + +class ContextObject(t.TypedDict): + ci_adapter: t.Optional[CIAdapterBase] + versioning_system: t.Optional[VersioningSystemInterface] + codecov_yaml: t.Optional[dict] + enterprise_url: t.Optional[str] + + +class CommandContext(click.Context): + obj: ContextObject + class UploadCollectionResultFile(object): def __init__(self, path: pathlib.Path): @@ -31,17 +47,17 @@ def __hash__(self) -> int: class UploadCollectionResultFileFixer(object): __slots__ = ["path", "fixed_lines_without_reason", "fixed_lines_with_reason", "eof"] path: pathlib.Path - fixed_lines_without_reason: typing.Set[int] - fixed_lines_with_reason: typing.Optional[typing.Set[typing.Tuple[int, str]]] - eof: typing.Optional[int] + fixed_lines_without_reason: t.Set[int] + fixed_lines_with_reason: t.Optional[t.Set[t.Tuple[int, str]]] + eof: t.Optional[int] @dataclass class UploadCollectionResult(object): __slots__ = ["network", "files", "file_fixes"] - network: typing.List[str] - files: typing.List[UploadCollectionResultFile] - file_fixes: typing.List[UploadCollectionResultFileFixer] + network: t.List[str] + files: t.List[UploadCollectionResultFile] + file_fixes: t.List[UploadCollectionResultFileFixer] class PreparationPluginInterface(object): @@ -59,14 +75,14 @@ class RequestResultWarning(object): class RequestError(object): __slots__ = ("code", "params", "description") code: str - params: typing.Dict + params: t.Dict description: str @dataclass class RequestResult(object): __slots__ = ("error", "warnings", "status_code", "text") - error: typing.Optional[RequestError] - warnings: typing.List[RequestResultWarning] + error: t.Optional[RequestError] + warnings: t.List[RequestResultWarning] status_code: int text: str From 1ad6ca917eed954026d8f44ced411c85a6c44bfd Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:21:48 -0400 Subject: [PATCH 095/128] fix: update junit XML file finder pattern (#503) --- codecov_cli/services/upload/file_finder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index f641f039..a0a1db45 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -36,7 +36,8 @@ ] test_results_files_patterns = [ - "*junit.xml", + "*junit*.xml", + "*test*.xml", ] coverage_files_excluded_patterns = [ From 6269e67107ed0bc2353d1bf89dfe1c19ffd2c09e Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:21:18 -0500 Subject: [PATCH 096/128] fix: first pass with tests (#506) --- codecov_cli/commands/base_picking.py | 12 ++--- codecov_cli/commands/commit.py | 14 ++--- codecov_cli/commands/create_report_result.py | 13 ++--- codecov_cli/commands/empty_upload.py | 13 ++--- codecov_cli/commands/get_report_results.py | 10 +--- codecov_cli/commands/labelanalysis.py | 12 ++--- codecov_cli/commands/process_test_results.py | 24 +++++++-- codecov_cli/commands/report.py | 12 ++--- codecov_cli/commands/send_notifications.py | 11 ++-- codecov_cli/commands/staticanalysis.py | 14 ++--- codecov_cli/commands/upload.py | 31 ++--------- codecov_cli/commands/upload_process.py | 29 ++--------- codecov_cli/helpers/args.py | 31 +++++++++++ codecov_cli/main.py | 2 + codecov_cli/services/commit/__init__.py | 21 ++++++-- codecov_cli/services/commit/base_picking.py | 3 +- codecov_cli/services/empty_upload/__init__.py | 16 +++++- codecov_cli/services/report/__init__.py | 30 +++++++++-- .../services/staticanalysis/__init__.py | 1 + codecov_cli/services/upload/__init__.py | 2 + .../services/upload/legacy_upload_sender.py | 7 ++- codecov_cli/services/upload/upload_sender.py | 8 +-- .../services/upload_completion/__init__.py | 13 ++++- tests/commands/test_process_test_results.py | 5 +- tests/helpers/test_args.py | 51 +++++++++++++++++++ tests/helpers/test_upload_sender.py | 6 ++- tests/services/commit/test_commit_service.py | 9 +++- .../empty_upload/test_empty_upload.py | 10 ++-- tests/services/report/test_report_results.py | 6 ++- tests/services/report/test_report_service.py | 7 +-- .../test_static_analysis_service.py | 6 +++ tests/services/upload/test_upload_service.py | 5 ++ 32 files changed, 268 insertions(+), 166 deletions(-) create mode 100644 codecov_cli/helpers/args.py create mode 100644 tests/helpers/test_args.py diff --git a/codecov_cli/commands/base_picking.py b/codecov_cli/commands/base_picking.py index 372bc888..3c536751 100644 --- a/codecov_cli/commands/base_picking.py +++ b/codecov_cli/commands/base_picking.py @@ -4,6 +4,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.encoder import slug_without_subgroups_is_invalid from codecov_cli.services.commit.base_picking import base_picking_logic from codecov_cli.types import CommandContext @@ -54,16 +55,11 @@ def pr_base_picking( service: typing.Optional[str], ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting base picking process", extra=dict( - extra_log_attributes=dict( - pr=pr, - slug=slug, - token=token, - service=service, - enterprise_url=enterprise_url, - ) + extra_log_attributes=args, ), ) @@ -73,4 +69,4 @@ def pr_base_picking( ) return - base_picking_logic(base_sha, pr, slug, token, service, enterprise_url) + base_picking_logic(base_sha, pr, slug, token, service, enterprise_url, args) diff --git a/codecov_cli/commands/commit.py b/codecov_cli/commands/commit.py index 316fbdff..b2b14a7e 100644 --- a/codecov_cli/commands/commit.py +++ b/codecov_cli/commands/commit.py @@ -4,6 +4,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.commit import create_commit_logic @@ -47,19 +48,11 @@ def create_commit( fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting create commit process", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - parent_sha=parent_sha, - pr=pull_request_number, - branch=branch, - slug=slug, - token=token, - service=git_service, - enterprise_url=enterprise_url, - ) + extra_log_attributes=args, ), ) create_commit_logic( @@ -72,4 +65,5 @@ def create_commit( git_service, enterprise_url, fail_on_error, + args, ) diff --git a/codecov_cli/commands/create_report_result.py b/codecov_cli/commands/create_report_result.py index 27dae8da..28648f23 100644 --- a/codecov_cli/commands/create_report_result.py +++ b/codecov_cli/commands/create_report_result.py @@ -2,6 +2,7 @@ import click +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_results_logic from codecov_cli.types import CommandContext @@ -25,19 +26,13 @@ def create_report_results( fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Creating report results", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - code=code, - slug=slug, - service=git_service, - enterprise_url=enterprise_url, - token=token, - ) + extra_log_attributes=args, ), ) create_report_results_logic( - commit_sha, code, slug, git_service, token, enterprise_url, fail_on_error + commit_sha, code, slug, git_service, token, enterprise_url, fail_on_error, args ) diff --git a/codecov_cli/commands/empty_upload.py b/codecov_cli/commands/empty_upload.py index 7af5a246..d68e0224 100644 --- a/codecov_cli/commands/empty_upload.py +++ b/codecov_cli/commands/empty_upload.py @@ -4,6 +4,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.empty_upload import empty_upload_logic @@ -26,19 +27,13 @@ def empty_upload( fail_on_error: typing.Optional[bool], ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting empty upload process", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - slug=slug, - token=token, - service=git_service, - enterprise_url=enterprise_url, - fail_on_error=fail_on_error, - ) + extra_log_attributes=args, ), ) return empty_upload_logic( - commit_sha, slug, token, git_service, enterprise_url, fail_on_error, force + commit_sha, slug, token, git_service, enterprise_url, fail_on_error, force, args ) diff --git a/codecov_cli/commands/get_report_results.py b/codecov_cli/commands/get_report_results.py index 6487dd0c..017025d1 100644 --- a/codecov_cli/commands/get_report_results.py +++ b/codecov_cli/commands/get_report_results.py @@ -28,17 +28,11 @@ def get_report_results( fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Getting report results", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - code=code, - slug=slug, - service=git_service, - enterprise_url=enterprise_url, - token=token, - ) + extra_log_attributes=args, ), ) encoded_slug = encode_slug(slug) diff --git a/codecov_cli/commands/labelanalysis.py b/codecov_cli/commands/labelanalysis.py index ec1cfb24..cb664994 100644 --- a/codecov_cli/commands/labelanalysis.py +++ b/codecov_cli/commands/labelanalysis.py @@ -9,6 +9,7 @@ from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum from codecov_cli.helpers import request +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.validators import validate_commit_sha from codecov_cli.runners import get_runner @@ -89,18 +90,11 @@ def label_analysis( runner_params: List[str], ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting label analysis", extra=dict( - extra_log_attributes=dict( - head_commit_sha=head_commit_sha, - base_commit_sha=base_commit_sha, - token=token, - runner_name=runner_name, - enterprise_url=enterprise_url, - max_wait_time=max_wait_time, - dry_run=dry_run, - ) + extra_log_attributes=args, ), ) if head_commit_sha == base_commit_sha: diff --git a/codecov_cli/commands/process_test_results.py b/codecov_cli/commands/process_test_results.py index 1bf4558a..f887c5b9 100644 --- a/codecov_cli/commands/process_test_results.py +++ b/codecov_cli/commands/process_test_results.py @@ -13,11 +13,13 @@ parse_junit_xml, ) +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.request import ( log_warnings_and_errors_if_any, send_post_request, ) from codecov_cli.services.upload.file_finder import select_file_finder +from codecov_cli.types import CommandContext logger = logging.getLogger("codecovcli") @@ -83,8 +85,14 @@ class TestResultsNotificationPayload: @click.command() @process_test_results_options +@click.pass_context def process_test_results( - dir=None, files=None, exclude_folders=None, disable_search=None, provider_token=None + ctx: CommandContext, + dir=None, + files=None, + exclude_folders=None, + disable_search=None, + provider_token=None, ): if provider_token is None: raise click.ClickException( @@ -130,10 +138,11 @@ def process_test_results( # GITHUB_REF is documented here: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables pr_number = ref.split("/")[2] - create_github_comment(provider_token, slug, pr_number, message) + args = get_cli_args(ctx) + create_github_comment(provider_token, slug, pr_number, message, args) -def create_github_comment(token, repo_slug, pr_number, message): +def create_github_comment(token, repo_slug, pr_number, message, args): url = f"https://api.github.com/repos/{repo_slug}/issues/{pr_number}/comments" headers = { @@ -144,7 +153,14 @@ def create_github_comment(token, repo_slug, pr_number, message): logger.info("Posting github comment") log_warnings_and_errors_if_any( - send_post_request(url=url, data={"body": message}, headers=headers), + send_post_request( + url=url, + data={ + "body": message, + "cli_args": args, + }, + headers=headers, + ), "Posting test results comment", ) diff --git a/codecov_cli/commands/report.py b/codecov_cli/commands/report.py index 25d63194..7169ba4f 100644 --- a/codecov_cli/commands/report.py +++ b/codecov_cli/commands/report.py @@ -3,6 +3,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.options import global_options from codecov_cli.services.report import create_report_logic from codecov_cli.types import CommandContext @@ -36,17 +37,11 @@ def create_report( pull_request_number: int, ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting create report process", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - code=code, - slug=slug, - service=git_service, - enterprise_url=enterprise_url, - token=token, - ) + extra_log_attributes=args, ), ) res = create_report_logic( @@ -58,6 +53,7 @@ def create_report( enterprise_url, pull_request_number, fail_on_error, + args, ) if not res.error: logger.info( diff --git a/codecov_cli/commands/send_notifications.py b/codecov_cli/commands/send_notifications.py index 7798a6e3..0b7b79d1 100644 --- a/codecov_cli/commands/send_notifications.py +++ b/codecov_cli/commands/send_notifications.py @@ -4,6 +4,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.upload_completion import upload_completion_logic @@ -24,16 +25,11 @@ def send_notifications( fail_on_error: bool, ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Sending notifications process has started", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - slug=slug, - token=token, - service=git_service, - enterprise_url=enterprise_url, - ) + extra_log_attributes=args, ), ) return upload_completion_logic( @@ -43,4 +39,5 @@ def send_notifications( git_service, enterprise_url, fail_on_error, + args, ) diff --git a/codecov_cli/commands/staticanalysis.py b/codecov_cli/commands/staticanalysis.py index 265efcc9..a876f90b 100644 --- a/codecov_cli/commands/staticanalysis.py +++ b/codecov_cli/commands/staticanalysis.py @@ -6,6 +6,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.validators import validate_commit_sha from codecov_cli.services.staticanalysis import run_analysis_entrypoint from codecov_cli.types import CommandContext @@ -59,19 +60,11 @@ def static_analysis( folders_to_exclude: typing.List[pathlib.Path], ): enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting Static Analysis processing", extra=dict( - extra_log_attributes=dict( - foldertosearch=foldertosearch, - numberprocesses=numberprocesses, - pattern=pattern, - commit_sha=commit, - token=token, - force=force, - folders_to_exclude=folders_to_exclude, - enterprise_url=enterprise_url, - ) + extra_log_attributes=args, ), ) return asyncio.run( @@ -85,5 +78,6 @@ def static_analysis( force, list(folders_to_exclude), enterprise_url, + args, ) ) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index c14f5614..dfbfe431 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -6,6 +6,7 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.options import global_options from codecov_cli.services.upload import do_upload_logic from codecov_cli.types import CommandContext @@ -225,36 +226,11 @@ def do_upload( cli_config = codecov_yaml.get("cli", {}) ci_adapter = ctx.obj.get("ci_adapter") enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) logger.debug( "Starting upload processing", extra=dict( - extra_log_attributes=dict( - branch=branch, - build_code=build_code, - build_url=build_url, - commit_sha=commit_sha, - disable_file_fixes=disable_file_fixes, - disable_search=disable_search, - enterprise_url=enterprise_url, - env_vars=env_vars, - files_search_exclude_folders=files_search_exclude_folders, - files_search_explicitly_listed_files=files_search_explicitly_listed_files, - files_search_root_folder=files_search_root_folder, - flags=flags, - git_service=git_service, - handle_no_reports_found=handle_no_reports_found, - job_code=job_code, - name=name, - network_filter=network_filter, - network_prefix=network_prefix, - network_root_folder=network_root_folder, - plugin_names=plugin_names, - pull_request_number=pull_request_number, - report_code=report_code, - slug=slug, - token=token, - upload_file_type=report_type, - ) + extra_log_attributes=args, ), ) do_upload_logic( @@ -289,4 +265,5 @@ def do_upload( token=token, upload_file_type=report_type, use_legacy_uploader=use_legacy_uploader, + args=args, ) diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index 2d12a587..e6efecc2 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -7,6 +7,7 @@ from codecov_cli.commands.commit import create_commit from codecov_cli.commands.report import create_report from codecov_cli.commands.upload import do_upload, global_upload_options +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.options import global_options from codecov_cli.types import CommandContext @@ -53,35 +54,11 @@ def upload_process( handle_no_reports_found: bool, report_type: str, ): + args = get_cli_args(ctx) logger.debug( "Starting upload process", extra=dict( - extra_log_attributes=dict( - commit_sha=commit_sha, - report_code=report_code, - build_code=build_code, - build_url=build_url, - job_code=job_code, - env_vars=env_vars, - flags=flags, - name=name, - network_filter=network_filter, - network_prefix=network_prefix, - network_root_folder=network_root_folder, - files_search_root_folder=files_search_root_folder, - files_search_exclude_folders=files_search_exclude_folders, - files_search_explicitly_listed_files=files_search_explicitly_listed_files, - plugin_names=plugin_names, - token=token, - branch=branch, - slug=slug, - pull_request_number=pull_request_number, - git_service=git_service, - disable_search=disable_search, - disable_file_fixes=disable_file_fixes, - fail_on_error=fail_on_error, - handle_no_reports_found=handle_no_reports_found, - ) + extra_log_attributes=args, ), ) diff --git a/codecov_cli/helpers/args.py b/codecov_cli/helpers/args.py new file mode 100644 index 00000000..74c82f7b --- /dev/null +++ b/codecov_cli/helpers/args.py @@ -0,0 +1,31 @@ +import json +import logging +from pathlib import PosixPath + +import click + +from codecov_cli import __version__ + +logger = logging.getLogger("codecovcli") + + +def get_cli_args(ctx: click.Context): + args = ctx.obj["cli_args"] + args["command"] = str(ctx.command.name) + args["version"] = f"cli-{__version__}" + args.update(ctx.params) + if "token" in args: + del args["token"] + + filtered_args = {} + for k in args.keys(): + try: + if type(args[k]) == PosixPath: + filtered_args[k] = str(args[k]) + else: + json.dumps(args[k]) + filtered_args[k] = args[k] + except Exception as e: + continue + + return filtered_args diff --git a/codecov_cli/main.py b/codecov_cli/main.py index e1a50728..9505aaa6 100644 --- a/codecov_cli/main.py +++ b/codecov_cli/main.py @@ -51,6 +51,8 @@ def cli( enterprise_url: str, verbose: bool = False, ): + ctx.obj["cli_args"] = ctx.params + ctx.obj["cli_args"]["version"] = f"cli-{__version__}" configure_logger(logger, log_level=(logging.DEBUG if verbose else logging.INFO)) ctx.help_option_names = ["-h", "--help"] ctx.obj["ci_adapter"] = get_ci_adapter(auto_load_params_from) diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index a465c0df..85c4d256 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -23,6 +23,7 @@ def create_commit_logic( service: typing.Optional[str], enterprise_url: typing.Optional[str] = None, fail_on_error: bool = False, + args: dict = None, ): encoded_slug = encode_slug(slug) sending_result = send_commit_data( @@ -34,6 +35,7 @@ def create_commit_logic( token=token, service=service, enterprise_url=enterprise_url, + args=args, ) log_warnings_and_errors_if_any(sending_result, "Commit creating", fail_on_error) @@ -41,7 +43,15 @@ def create_commit_logic( def send_commit_data( - commit_sha, parent_sha, pr, branch, slug, token, service, enterprise_url + commit_sha, + parent_sha, + pr, + branch, + slug, + token, + service, + enterprise_url, + args, ): # this is how the CLI receives the username of the user to whom the fork belongs # to and the branch name from the action @@ -54,12 +64,17 @@ def send_commit_data( headers = get_token_header_or_fail(token) data = { + "branch": branch, + "cli_args": args, "commitid": commit_sha, "parent_commit_id": parent_sha, "pullid": pr, - "branch": branch, } upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{slug}/commits" - return send_post_request(url=url, data=data, headers=headers) + return send_post_request( + url=url, + data=data, + headers=headers, + ) diff --git a/codecov_cli/services/commit/base_picking.py b/codecov_cli/services/commit/base_picking.py index 811b2d7b..20767132 100644 --- a/codecov_cli/services/commit/base_picking.py +++ b/codecov_cli/services/commit/base_picking.py @@ -10,8 +10,9 @@ logger = logging.getLogger("codecovcli") -def base_picking_logic(base_sha, pr, slug, token, service, enterprise_url): +def base_picking_logic(base_sha, pr, slug, token, service, enterprise_url, args): data = { + "cli_args": args, "user_provided_base_sha": base_sha, } headers = get_token_header_or_fail(token) diff --git a/codecov_cli/services/empty_upload/__init__.py b/codecov_cli/services/empty_upload/__init__.py index 57bb32b9..7c8b0682 100644 --- a/codecov_cli/services/empty_upload/__init__.py +++ b/codecov_cli/services/empty_upload/__init__.py @@ -13,14 +13,26 @@ def empty_upload_logic( - commit_sha, slug, token, git_service, enterprise_url, fail_on_error, should_force + commit_sha, + slug, + token, + git_service, + enterprise_url, + fail_on_error, + should_force, + args, ): encoded_slug = encode_slug(slug) headers = get_token_header_or_fail(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/empty-upload" sending_result = send_post_request( - url=url, headers=headers, data={"should_force": should_force} + url=url, + headers=headers, + data={ + "cli_args": args, + "should_force": should_force, + }, ) log_warnings_and_errors_if_any(sending_result, "Empty Upload", fail_on_error) if sending_result.status_code == 200: diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index b8e7c445..a3734ad1 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -28,6 +28,7 @@ def create_report_logic( enterprise_url: str, pull_request_number: int, fail_on_error: bool = False, + args: dict = None, ): encoded_slug = encode_slug(slug) sending_result = send_create_report_request( @@ -38,15 +39,26 @@ def create_report_logic( encoded_slug, enterprise_url, pull_request_number, + args, ) log_warnings_and_errors_if_any(sending_result, "Report creating", fail_on_error) return sending_result def send_create_report_request( - commit_sha, code, service, token, encoded_slug, enterprise_url, pull_request_number + commit_sha, + code, + service, + token, + encoded_slug, + enterprise_url, + pull_request_number, + args, ): - data = {"code": code} + data = { + "cli_args": args, + "code": code, + } headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" @@ -61,6 +73,7 @@ def create_report_results_logic( token: str, enterprise_url: str, fail_on_error: bool = False, + args: dict = None, ): encoded_slug = encode_slug(slug) sending_result = send_reports_result_request( @@ -79,12 +92,21 @@ def create_report_results_logic( def send_reports_result_request( - commit_sha, report_code, encoded_slug, service, token, enterprise_url + commit_sha, + report_code, + encoded_slug, + service, + token, + enterprise_url, + args, ): + data = { + "cli_args": args, + } headers = get_token_header_or_fail(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/results" - return send_post_request(url=url, headers=headers) + return send_post_request(url=url, data=data, headers=headers) def send_reports_result_get_request( diff --git a/codecov_cli/services/staticanalysis/__init__.py b/codecov_cli/services/staticanalysis/__init__.py index f82457df..aedd82c1 100644 --- a/codecov_cli/services/staticanalysis/__init__.py +++ b/codecov_cli/services/staticanalysis/__init__.py @@ -33,6 +33,7 @@ async def run_analysis_entrypoint( should_force: bool, folders_to_exclude: typing.List[Path], enterprise_url: typing.Optional[str], + args: dict, ): ff = select_file_finder(config) files = list(ff.find_files(folder, pattern, folders_to_exclude)) diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index a8fc64d7..411c820c 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -25,6 +25,7 @@ def do_upload_logic( versioning_system: VersioningSystemInterface, ci_adapter: CIAdapterBase, *, + args: dict = None, branch: typing.Optional[str], build_code: typing.Optional[str], build_url: typing.Optional[str], @@ -127,6 +128,7 @@ def do_upload_logic( ci_service, git_service, enterprise_url, + args, ) else: logger.info("dry-run option activated. NOT sending data to Codecov.") diff --git a/codecov_cli/services/upload/legacy_upload_sender.py b/codecov_cli/services/upload/legacy_upload_sender.py index 283b204d..67711ef3 100644 --- a/codecov_cli/services/upload/legacy_upload_sender.py +++ b/codecov_cli/services/upload/legacy_upload_sender.py @@ -51,6 +51,7 @@ def send_upload_data( ci_service: typing.Optional[str] = None, git_service: typing.Optional[str] = None, enterprise_url: typing.Optional[str] = None, + args: dict = None, ) -> UploadSendingResult: params = { "package": f"codecov-cli/{codecov_cli_version}", @@ -72,9 +73,13 @@ def send_upload_data( logger.warning("Token is empty.") headers = {"X-Upload-Token": ""} + data = { + "cli_args": args, + } + upload_url = enterprise_url or LEGACY_CODECOV_API_URL resp = send_post_request( - f"{upload_url}/upload/v4", headers=headers, params=params + f"{upload_url}/upload/v4", data=data, headers=headers, params=params ) if resp.status_code >= 400: return resp diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index 4b9817d2..22f8924a 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -42,15 +42,17 @@ def send_upload_data( ci_service: typing.Optional[str] = None, git_service: typing.Optional[str] = None, enterprise_url: typing.Optional[str] = None, + args: dict = None, ) -> RequestResult: data = { + "ci_service": ci_service, "ci_url": build_url, - "flags": flags, + "cli_args": args, "env": env_vars, - "name": name, + "flags": flags, "job_code": job_code, + "name": name, "version": codecov_cli_version, - "ci_service": ci_service, } headers = get_token_header(token) encoded_slug = encode_slug(slug) diff --git a/codecov_cli/services/upload_completion/__init__.py b/codecov_cli/services/upload_completion/__init__.py index 20f68872..b595ba7f 100644 --- a/codecov_cli/services/upload_completion/__init__.py +++ b/codecov_cli/services/upload_completion/__init__.py @@ -13,13 +13,22 @@ def upload_completion_logic( - commit_sha, slug, token, git_service, enterprise_url, fail_on_error=False + commit_sha, + slug, + token, + git_service, + enterprise_url, + fail_on_error=False, + args=None, ): encoded_slug = encode_slug(slug) headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/upload-complete" - sending_result = send_post_request(url=url, headers=headers) + data = { + "cli_args": args, + } + sending_result = send_post_request(url=url, data=data, headers=headers) log_warnings_and_errors_if_any( sending_result, "Upload Completion", fail_on_error=fail_on_error ) diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index d0c62eb7..ca073037 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -48,7 +48,8 @@ def test_process_test_results( mocked_post.assert_called_with( url="https://api.github.com/repos/fake/repo/issues/pull/comments", data={ - "body": "### :x: Failed Test Results: \nCompleted 4 tests with **`1 failed`**, 3 passed and 0 skipped.\n
View the full list of failed tests\n\n| **Test Description** | **Failure message** |\n| :-- | :-- |\n|
Testsuite:
api.temp.calculator.test_calculator::test_divide

Test name:
pytest
|
def
test_divide():
&gt; assert Calculator.divide(1, 2) == 0.5
E assert 1.0 == 0.5
E + where 1.0 = &lt;function Calculator.divide at 0x104c9eb90&gt;(1, 2)
E + where &lt;function Calculator.divide at 0x104c9eb90&gt; = Calculator.divide
.../temp/calculator/test_calculator.py:30: AssertionError
|" + "body": "### :x: Failed Test Results: \nCompleted 4 tests with **`1 failed`**, 3 passed and 0 skipped.\n
View the full list of failed tests\n\n| **Test Description** | **Failure message** |\n| :-- | :-- |\n|
Testsuite:
api.temp.calculator.test_calculator::test_divide

Test name:
pytest
|
def
test_divide():
&gt; assert Calculator.divide(1, 2) == 0.5
E assert 1.0 == 0.5
E + where 1.0 = &lt;function Calculator.divide at 0x104c9eb90&gt;(1, 2)
E + where &lt;function Calculator.divide at 0x104c9eb90&gt; = Calculator.divide
.../temp/calculator/test_calculator.py:30: AssertionError
|", + "cli_args": {'auto_load_params_from': None, 'codecov_yml_path': None, 'enterprise_url': None, 'verbose': False, 'version': 'cli-0.7.4', 'command': 'process-test-results', 'provider_token': 'whatever', 'disable_search': True, 'dir': os.getcwd(), 'exclude_folders': ()}, }, headers={ "Accept": "application/vnd.github+json", @@ -221,4 +222,4 @@ def test_process_test_results_missing_step_summary(mocker, tmpdir): "Error: Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable.", ] for log in expected_logs: - assert log in result.output \ No newline at end of file + assert log in result.output diff --git a/tests/helpers/test_args.py b/tests/helpers/test_args.py new file mode 100644 index 00000000..503b877e --- /dev/null +++ b/tests/helpers/test_args.py @@ -0,0 +1,51 @@ +import os +from pathlib import PosixPath + +import click + +from codecov_cli import __version__ +from codecov_cli.helpers.args import get_cli_args + + +def test_get_cli_args(): + ctx = click.Context(click.Command("do-upload")) + ctx.obj = {} + ctx.obj["cli_args"] = { + "verbose": True, + } + ctx.params = { + "branch": "fake_branch", + "token": "fakeTOKEN", + } + + expected = { + "branch": "fake_branch", + "command": "do-upload", + "verbose": True, + "version": f"cli-{__version__}", + } + + assert get_cli_args(ctx) == expected + + +def test_get_cli_args_with_posix(): + ctx = click.Context(click.Command("do-upload")) + ctx.obj = {} + ctx.obj["cli_args"] = { + "verbose": True, + } + ctx.params = { + "branch": "fake_branch", + "path": PosixPath(os.getcwd()), + "token": "fakeTOKEN", + } + + expected = { + "branch": "fake_branch", + "command": "do-upload", + "path": str(PosixPath(os.getcwd())), + "verbose": True, + "version": f"cli-{__version__}", + } + + assert get_cli_args(ctx) == expected diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 14195a43..9033082c 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -16,6 +16,7 @@ random_token = "f359afb9-8a2a-42ab-a448-c3d267ff495b" random_sha = "845548c6b95223f12e8317a1820705f64beaf69e" named_upload_data = { + "args": None, "upload_file_type": "coverage", "report_code": "report_code", "env_vars": {}, @@ -46,13 +47,14 @@ "git_service": "github", } request_data = { + "ci_service": "ci_service", "ci_url": "build_url", + "cli_args": None, "env": {}, "flags": "flags", "job_code": "job_code", "name": "name", "version": codecov_cli_version, - "ci_service": "ci_service", } @@ -232,7 +234,7 @@ def test_upload_sender_post_called_with_right_parameters_tokenless( mocker, ): headers = {} - + mocked_legacy_upload_endpoint.match = [ matchers.json_params_matcher(request_data), matchers.header_matcher(headers), diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 240040c2..3f4ae1fd 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -48,6 +48,7 @@ def test_commit_command_with_warnings(mocker): token="token", service="service", enterprise_url=None, + args=None, ) @@ -76,6 +77,7 @@ def test_commit_command_with_error(mocker): token="token", service="service", enterprise_url=None, + args={}, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) @@ -96,6 +98,7 @@ def test_commit_command_with_error(mocker): token="token", service="service", enterprise_url=None, + args={}, ) @@ -114,6 +117,7 @@ def test_commit_sender_200(mocker): token, "service", None, + None, ) assert res.error is None assert res.warnings == [] @@ -135,6 +139,7 @@ def test_commit_sender_403(mocker): token, "service", None, + None, ) assert res.error == RequestError( code="HTTP Error 403", @@ -160,14 +165,16 @@ def test_commit_sender_with_forked_repo(mocker): None, "github", None, + None, ) mocked_response.assert_called_with( url="https://api.codecov.io/upload/github/codecov::::codecov-cli/commits", data={ + "branch": "user_forked_repo/codecov-cli:branch", + "cli_args": None, "commitid": "commit_sha", "parent_commit_id": "parent_sha", "pullid": "1", - "branch": "user_forked_repo/codecov-cli:branch", }, headers=None, ) diff --git a/tests/services/empty_upload/test_empty_upload.py b/tests/services/empty_upload/test_empty_upload.py index cc714470..16f9f946 100644 --- a/tests/services/empty_upload/test_empty_upload.py +++ b/tests/services/empty_upload/test_empty_upload.py @@ -21,7 +21,7 @@ def test_empty_upload_with_warnings(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False + "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False, None ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -50,7 +50,7 @@ def test_empty_upload_with_error(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False + "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False, None ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) @@ -77,7 +77,7 @@ def test_empty_upload_200(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", token, "service", None, False, False + "commit_sha", "owner/repo", token, "service", None, False, False, None ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -97,7 +97,7 @@ def test_empty_upload_403(mocker): ) token = uuid.uuid4() res = empty_upload_logic( - "commit_sha", "owner/repo", token, "service", None, False, False + "commit_sha", "owner/repo", token, "service", None, False, False, None ) assert res.error == RequestError( code="HTTP Error 403", @@ -122,7 +122,7 @@ def test_empty_upload_force(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", token, "service", None, False, True + "commit_sha", "owner/repo", token, "service", None, False, True, None ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ diff --git a/tests/services/report/test_report_results.py b/tests/services/report/test_report_results.py index 3b22cc0d..27808d23 100644 --- a/tests/services/report/test_report_results.py +++ b/tests/services/report/test_report_results.py @@ -31,6 +31,7 @@ def test_report_results_command_with_warnings(mocker): slug="owner/repo", token="token", enterprise_url=None, + args=None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) @@ -74,6 +75,7 @@ def test_report_results_command_with_error(mocker): slug="owner/repo", token="token", enterprise_url=None, + args=None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) @@ -99,7 +101,7 @@ def test_report_results_request_200(mocker): ) token = uuid.uuid4() res = send_reports_result_request( - "commit_sha", "report_code", "encoded_slug", "service", token, None + "commit_sha", "report_code", "encoded_slug", "service", token, None, None ) assert res.error is None assert res.warnings == [] @@ -113,7 +115,7 @@ def test_report_results_403(mocker): ) token = uuid.uuid4() res = send_reports_result_request( - "commit_sha", "report_code", "encoded_slug", "service", token, None + "commit_sha", "report_code", "encoded_slug", "service", token, None, None ) assert res.error == RequestError( code="HTTP Error 403", diff --git a/tests/services/report/test_report_service.py b/tests/services/report/test_report_service.py index 8d31a112..b3a0f04a 100644 --- a/tests/services/report/test_report_service.py +++ b/tests/services/report/test_report_service.py @@ -20,6 +20,7 @@ def test_send_create_report_request_200(mocker): "owner::::repo", "enterprise_url", 1, + None, ) assert res.error is None assert res.warnings == [] @@ -32,7 +33,7 @@ def test_send_create_report_request_403(mocker): return_value=mocker.MagicMock(status_code=403, text="Permission denied"), ) res = send_create_report_request( - "commit_sha", "code", "github", uuid.uuid4(), "owner::::repo", None, 1 + "commit_sha", "code", "github", uuid.uuid4(), "owner::::repo", None, 1, None ) assert res.error == RequestError( code="HTTP Error 403", @@ -77,7 +78,7 @@ def test_create_report_command_with_warnings(mocker): text="", ) mocked_send_request.assert_called_with( - "commit_sha", "code", "github", "token", "owner::::repo", None, 1 + "commit_sha", "code", "github", "token", "owner::::repo", None, 1, None ) @@ -123,5 +124,5 @@ def test_create_report_command_with_error(mocker): warnings=[], ) mock_send_report_data.assert_called_with( - "commit_sha", "code", "github", "token", "owner::::repo", "enterprise_url", 1 + "commit_sha", "code", "github", "token", "owner::::repo", "enterprise_url", 1, None ) diff --git a/tests/services/static_analysis/test_static_analysis_service.py b/tests/services/static_analysis/test_static_analysis_service.py index c8721d98..262a3130 100644 --- a/tests/services/static_analysis/test_static_analysis_service.py +++ b/tests/services/static_analysis/test_static_analysis_service.py @@ -136,6 +136,7 @@ async def side_effect(*args, **kwargs): should_force=False, folders_to_exclude=[], enterprise_url=None, + args=None ) mock_file_finder.assert_called_with({}) mock_file_finder.return_value.find_files.assert_called() @@ -211,6 +212,7 @@ async def side_effect(client, all_data, el): should_force=False, folders_to_exclude=[], enterprise_url=None, + args=None, ) assert "Unknown error cancelled the upload tasks." in str(exp.value) mock_file_finder.assert_called_with({}) @@ -384,6 +386,7 @@ async def side_effect(*args, **kwargs): should_force=False, folders_to_exclude=[], enterprise_url=None, + args=None, ) mock_file_finder.assert_called_with({}) mock_file_finder.return_value.find_files.assert_called() @@ -458,6 +461,7 @@ async def side_effect(*args, **kwargs): should_force=False, folders_to_exclude=[], enterprise_url=None, + args=None, ) mock_file_finder.assert_called_with({}) mock_file_finder.return_value.find_files.assert_called() @@ -534,6 +538,7 @@ async def side_effect(*args, **kwargs): should_force=True, folders_to_exclude=[], enterprise_url=None, + args=None, ) mock_file_finder.assert_called_with({}) mock_file_finder.return_value.find_files.assert_called() @@ -605,6 +610,7 @@ async def side_effect(*args, **kwargs): should_force=False, folders_to_exclude=[], enterprise_url=None, + args=None, ) mock_file_finder.assert_called_with({}) mock_file_finder.return_value.find_files.assert_called() diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 8f7ad3e4..4bba13e9 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -70,6 +70,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): pull_request_number="pr", git_service="git_service", enterprise_url=None, + args=None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -103,6 +104,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): "service", "git_service", None, + None, ) @@ -192,6 +194,7 @@ def test_do_upload_logic_happy_path(mocker): "service", "git_service", None, + None, ) @@ -534,6 +537,7 @@ def test_do_upload_logic_happy_path_test_results(mocker): pull_request_number="pr", git_service="git_service", enterprise_url=None, + args={"args": "fake_args"} ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -565,4 +569,5 @@ def test_do_upload_logic_happy_path_test_results(mocker): "service", "git_service", None, + {"args": "fake_args"} ) From 14741f0e17ba56e5a5bd8fa46556780c5a3c65d5 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Fri, 27 Sep 2024 14:09:55 +0200 Subject: [PATCH 097/128] Slightly improve `FileFinder` typing (#509) This fixes a bunch of typing errors, as well as auto-reformats a bunch of stuff via the precommit hook. --- codecov_cli/helpers/folder_searcher.py | 24 ++--- codecov_cli/services/upload/file_finder.py | 20 ++-- tests/ci_adapters/test_azure_pipelines.py | 2 +- tests/commands/test_process_test_results.py | 18 +++- tests/helpers/test_git.py | 2 - tests/helpers/test_network_finder.py | 93 +++++++++++++++++-- tests/helpers/test_request.py | 15 ++- tests/helpers/test_upload_sender.py | 8 +- .../empty_upload/test_empty_upload.py | 18 +++- tests/services/report/test_report_service.py | 9 +- .../test_static_analysis_service.py | 2 +- .../upload/test_coverage_file_finder.py | 8 +- tests/services/upload/test_upload_service.py | 46 +++++++-- 13 files changed, 205 insertions(+), 60 deletions(-) diff --git a/codecov_cli/helpers/folder_searcher.py b/codecov_cli/helpers/folder_searcher.py index dc28a572..cc87238a 100644 --- a/codecov_cli/helpers/folder_searcher.py +++ b/codecov_cli/helpers/folder_searcher.py @@ -2,13 +2,13 @@ import os import pathlib import re -import typing from fnmatch import translate +from typing import Generator, List, Optional, Pattern def _is_included( - filename_include_regex: typing.Pattern, - multipart_include_regex: typing.Optional[typing.Pattern], + filename_include_regex: Pattern, + multipart_include_regex: Optional[Pattern], path: pathlib.Path, ): return filename_include_regex.match(path.name) and ( @@ -18,8 +18,8 @@ def _is_included( def _is_excluded( - filename_exclude_regex: typing.Optional[typing.Pattern], - multipart_exclude_regex: typing.Optional[typing.Pattern], + filename_exclude_regex: Optional[Pattern], + multipart_exclude_regex: Optional[Pattern], path: pathlib.Path, ): return ( @@ -31,14 +31,14 @@ def _is_excluded( def search_files( folder_to_search: pathlib.Path, - folders_to_ignore: typing.List[str], + folders_to_ignore: List[str], *, - filename_include_regex: typing.Pattern, - filename_exclude_regex: typing.Optional[typing.Pattern] = None, - multipart_include_regex: typing.Optional[typing.Pattern] = None, - multipart_exclude_regex: typing.Optional[typing.Pattern] = None, + filename_include_regex: Pattern, + filename_exclude_regex: Optional[Pattern] = None, + multipart_include_regex: Optional[Pattern] = None, + multipart_exclude_regex: Optional[Pattern] = None, search_for_directories: bool = False, -) -> typing.Generator[pathlib.Path, None, None]: +) -> Generator[pathlib.Path, None, None]: """ " Searches for files or directories in a given folder @@ -85,7 +85,7 @@ def search_files( yield file_path -def globs_to_regex(patterns: typing.List[str]) -> typing.Optional[typing.Pattern]: +def globs_to_regex(patterns: List[str]) -> Optional[Pattern]: """ Converts a list of glob patterns to a combined ORed regex diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index a0a1db45..9c0a89cc 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -1,7 +1,7 @@ import logging import os -import typing from pathlib import Path +from typing import Iterable, List, Optional, Pattern from codecov_cli.helpers.folder_searcher import globs_to_regex, search_files from codecov_cli.types import UploadCollectionResultFile @@ -183,9 +183,9 @@ class FileFinder(object): def __init__( self, - search_root: Path = None, - folders_to_ignore: typing.List[str] = None, - explicitly_listed_files: typing.List[Path] = None, + search_root: Optional[Path] = None, + folders_to_ignore: Optional[List[str]] = None, + explicitly_listed_files: Optional[List[Path]] = None, disable_search: bool = False, report_type: str = "coverage", ): @@ -195,7 +195,7 @@ def __init__( self.disable_search = disable_search self.report_type = report_type - def find_files(self) -> typing.List[UploadCollectionResultFile]: + def find_files(self) -> List[UploadCollectionResultFile]: if self.report_type == "coverage": files_excluded_patterns = coverage_files_excluded_patterns files_patterns = coverage_files_patterns @@ -203,21 +203,21 @@ def find_files(self) -> typing.List[UploadCollectionResultFile]: files_excluded_patterns = test_results_files_excluded_patterns files_patterns = test_results_files_patterns regex_patterns_to_exclude = globs_to_regex(files_excluded_patterns) - files_paths = [] + assert regex_patterns_to_exclude # this is never `None` + files_paths: Iterable[Path] = [] user_files_paths = [] if self.explicitly_listed_files: user_files_paths = self.get_user_specified_files(regex_patterns_to_exclude) if not self.disable_search: regex_patterns_to_include = globs_to_regex(files_patterns) + assert regex_patterns_to_include # this is never `None` files_paths = search_files( self.search_root, default_folders_to_ignore + self.folders_to_ignore, filename_include_regex=regex_patterns_to_include, filename_exclude_regex=regex_patterns_to_exclude, ) - result_files = [ - UploadCollectionResultFile(path) for path in files_paths if files_paths - ] + result_files = [UploadCollectionResultFile(path) for path in files_paths] user_result_files = [ UploadCollectionResultFile(path) for path in user_files_paths @@ -226,7 +226,7 @@ def find_files(self) -> typing.List[UploadCollectionResultFile]: return list(set(result_files + user_result_files)) - def get_user_specified_files(self, regex_patterns_to_exclude): + def get_user_specified_files(self, regex_patterns_to_exclude: Pattern): user_filenames_to_include = [] files_excluded_but_user_includes = [] for file in self.explicitly_listed_files: diff --git a/tests/ci_adapters/test_azure_pipelines.py b/tests/ci_adapters/test_azure_pipelines.py index 5dd71d0f..6a635bfb 100644 --- a/tests/ci_adapters/test_azure_pipelines.py +++ b/tests/ci_adapters/test_azure_pipelines.py @@ -47,7 +47,7 @@ def test_detect(self, env_dict, expected, mocker): ( { AzurePipelinesEnvEnum.BUILD_SOURCEVERSION: "123456789000111", - AzurePipelinesEnvEnum.SYSTEM_PULLREQUEST_SOURCECOMMITID: "111000987654321" + AzurePipelinesEnvEnum.SYSTEM_PULLREQUEST_SOURCECOMMITID: "111000987654321", }, "111000987654321", ), diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index ca073037..97c6d956 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -44,12 +44,22 @@ def test_process_test_results( assert result.exit_code == 0 - mocked_post.assert_called_with( url="https://api.github.com/repos/fake/repo/issues/pull/comments", data={ "body": "### :x: Failed Test Results: \nCompleted 4 tests with **`1 failed`**, 3 passed and 0 skipped.\n
View the full list of failed tests\n\n| **Test Description** | **Failure message** |\n| :-- | :-- |\n|
Testsuite:
api.temp.calculator.test_calculator::test_divide

Test name:
pytest
|
def
test_divide():
&gt; assert Calculator.divide(1, 2) == 0.5
E assert 1.0 == 0.5
E + where 1.0 = &lt;function Calculator.divide at 0x104c9eb90&gt;(1, 2)
E + where &lt;function Calculator.divide at 0x104c9eb90&gt; = Calculator.divide
.../temp/calculator/test_calculator.py:30: AssertionError
|", - "cli_args": {'auto_load_params_from': None, 'codecov_yml_path': None, 'enterprise_url': None, 'verbose': False, 'version': 'cli-0.7.4', 'command': 'process-test-results', 'provider_token': 'whatever', 'disable_search': True, 'dir': os.getcwd(), 'exclude_folders': ()}, + "cli_args": { + "auto_load_params_from": None, + "codecov_yml_path": None, + "enterprise_url": None, + "verbose": False, + "version": "cli-0.7.4", + "command": "process-test-results", + "provider_token": "whatever", + "disable_search": True, + "dir": os.getcwd(), + "exclude_folders": (), + }, }, headers={ "Accept": "application/vnd.github+json", @@ -59,7 +69,6 @@ def test_process_test_results( ) - def test_process_test_results_non_existent_file(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") @@ -94,7 +103,7 @@ def test_process_test_results_non_existent_file(mocker, tmpdir): assert result.exit_code == 1 expected_logs = [ "ci service found", - 'Some files were not found', + "Some files were not found", ] for log in expected_logs: assert log in result.output @@ -183,7 +192,6 @@ def test_process_test_results_missing_ref(mocker, tmpdir): assert log in result.output - def test_process_test_results_missing_step_summary(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") diff --git a/tests/helpers/test_git.py b/tests/helpers/test_git.py index fdcc778e..cca8a60b 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -133,5 +133,3 @@ def test_get_git_service_class(): assert isinstance(git.get_git_service("github"), Github) assert git.get_git_service("gitlab") == None assert git.get_git_service("bitbucket") == None - - diff --git a/tests/helpers/test_network_finder.py b/tests/helpers/test_network_finder.py index 859abc7d..afbb08c2 100644 --- a/tests/helpers/test_network_finder.py +++ b/tests/helpers/test_network_finder.py @@ -12,11 +12,36 @@ def test_find_files(mocker, tmp_path): mocked_vs = MagicMock() mocked_vs.list_relevant_files.return_value = filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter=None, network_prefix=None, network_root_folder=tmp_path).find_files() == filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(False) == filtered_filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter=None, + network_prefix=None, + network_root_folder=tmp_path, + ).find_files() + == filenames + ) + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix="bello", + network_root_folder=tmp_path, + ).find_files(False) + == filtered_filenames + ) + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix="bello", + network_root_folder=tmp_path, + ).find_files(True) + == filenames + ) mocked_vs.list_relevant_files.assert_called_with(tmp_path) + def test_find_files_with_filter(mocker, tmp_path): filenames = ["hello/a.txt", "hello/c.txt", "bello/b.txt"] filtered_filenames = ["hello/a.txt", "hello/c.txt"] @@ -24,10 +49,27 @@ def test_find_files_with_filter(mocker, tmp_path): mocked_vs = MagicMock() mocked_vs.list_relevant_files.return_value = filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix=None, network_root_folder=tmp_path).find_files() == filtered_filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix=None, + network_root_folder=tmp_path, + ).find_files() + == filtered_filenames + ) + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix="bello", + network_root_folder=tmp_path, + ).find_files(True) + == filenames + ) mocked_vs.list_relevant_files.assert_called_with(tmp_path) + def test_find_files_with_prefix(mocker, tmp_path): filenames = ["hello/a.txt", "hello/c.txt", "bello/b.txt"] filtered_filenames = ["hellohello/a.txt", "hellohello/c.txt", "hellobello/b.txt"] @@ -35,10 +77,27 @@ def test_find_files_with_prefix(mocker, tmp_path): mocked_vs = MagicMock() mocked_vs.list_relevant_files.return_value = filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter=None, network_prefix="hello", network_root_folder=tmp_path).find_files() == filtered_filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter=None, + network_prefix="hello", + network_root_folder=tmp_path, + ).find_files() + == filtered_filenames + ) + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix="bello", + network_root_folder=tmp_path, + ).find_files(True) + == filenames + ) mocked_vs.list_relevant_files.assert_called_with(tmp_path) + def test_find_files_with_filter_and_prefix(mocker, tmp_path): filenames = ["hello/a.txt", "hello/c.txt", "bello/b.txt"] filtered_filenames = ["bellohello/a.txt", "bellohello/c.txt"] @@ -46,6 +105,22 @@ def test_find_files_with_filter_and_prefix(mocker, tmp_path): mocked_vs = MagicMock() mocked_vs.list_relevant_files.return_value = filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files() == filtered_filenames - assert NetworkFinder(versioning_system=mocked_vs, network_filter="hello", network_prefix="bello", network_root_folder=tmp_path).find_files(True) == filenames + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix="bello", + network_root_folder=tmp_path, + ).find_files() + == filtered_filenames + ) + assert ( + NetworkFinder( + versioning_system=mocked_vs, + network_filter="hello", + network_prefix="bello", + network_root_folder=tmp_path, + ).find_files(True) + == filenames + ) mocked_vs.list_relevant_files.assert_called_with(tmp_path) diff --git a/tests/helpers/test_request.py b/tests/helpers/test_request.py index 1125f93f..92dd6e6d 100644 --- a/tests/helpers/test_request.py +++ b/tests/helpers/test_request.py @@ -60,10 +60,12 @@ def test_log_result_without_token(mocker): error=None, warnings=[], status_code=201, - text="{\"message\":\"commit\",\"timestamp\":\"2024-03-25T15:41:07Z\",\"ci_passed\":true,\"state\":\"complete\",\"repository\":{\"name\":\"repo\",\"is_private\":false,\"active\":true,\"language\":\"python\",\"yaml\":null},\"author\":{\"avatar_url\":\"https://example.com\",\"service\":\"github\",\"username\":null,\"name\":\"dependabot[bot]\",\"ownerid\":2780265},\"commitid\":\"commit\",\"parent_commit_id\":\"parent\",\"pullid\":1,\"branch\":\"main\"}" + text='{"message":"commit","timestamp":"2024-03-25T15:41:07Z","ci_passed":true,"state":"complete","repository":{"name":"repo","is_private":false,"active":true,"language":"python","yaml":null},"author":{"avatar_url":"https://example.com","service":"github","username":null,"name":"dependabot[bot]","ownerid":2780265},"commitid":"commit","parent_commit_id":"parent","pullid":1,"branch":"main"}', ) log_warnings_and_errors_if_any(result, "Commit creating", False) - mock_log_debug.assert_called_with('Commit creating result', extra={'extra_log_attributes': {'result': result}}) + mock_log_debug.assert_called_with( + "Commit creating result", extra={"extra_log_attributes": {"result": result}} + ) def test_log_result_with_token(mocker): @@ -72,10 +74,10 @@ def test_log_result_with_token(mocker): error=None, warnings=[], status_code=201, - text="{\"message\": \"commit\", \"timestamp\": \"2024-07-16T20:51:07Z\", \"ci_passed\": true, \"state\": \"complete\", \"repository\": {\"name\": \"repo\", \"is_private\": false, \"active\": true, \"language\": \"python\", \"yaml\": {\"codecov\": {\"token\": \"faketoken\"}}, \"author\": {\"avatar_url\": \"https://example.com\", \"service\": \"github\", \"username\": \"author\", \"name\": \"author\", \"ownerid\": 3461769}, \"commitid\": \"commit\", \"parent_commit_id\": \"parent_commit\", \"pullid\": null, \"branch\": \"main\"}}" + text='{"message": "commit", "timestamp": "2024-07-16T20:51:07Z", "ci_passed": true, "state": "complete", "repository": {"name": "repo", "is_private": false, "active": true, "language": "python", "yaml": {"codecov": {"token": "faketoken"}}, "author": {"avatar_url": "https://example.com", "service": "github", "username": "author", "name": "author", "ownerid": 3461769}, "commitid": "commit", "parent_commit_id": "parent_commit", "pullid": null, "branch": "main"}}', ) - expected_text = "{\"message\": \"commit\", \"timestamp\": \"2024-07-16T20:51:07Z\", \"ci_passed\": true, \"state\": \"complete\", \"repository\": {\"name\": \"repo\", \"is_private\": false, \"active\": true, \"language\": \"python\", \"yaml\": {\"codecov\": {\"token\": \"f******************\"}}, \"author\": {\"avatar_url\": \"https://example.com\", \"service\": \"github\", \"username\": \"author\", \"name\": \"author\", \"ownerid\": 3461769}, \"commitid\": \"commit\", \"parent_commit_id\": \"parent_commit\", \"pullid\": null, \"branch\": \"main\"}}" + expected_text = '{"message": "commit", "timestamp": "2024-07-16T20:51:07Z", "ci_passed": true, "state": "complete", "repository": {"name": "repo", "is_private": false, "active": true, "language": "python", "yaml": {"codecov": {"token": "f******************"}}, "author": {"avatar_url": "https://example.com", "service": "github", "username": "author", "name": "author", "ownerid": 3461769}, "commitid": "commit", "parent_commit_id": "parent_commit", "pullid": null, "branch": "main"}}' expected = RequestResult( error=None, warnings=[], @@ -83,7 +85,9 @@ def test_log_result_with_token(mocker): text=expected_text, ) log_warnings_and_errors_if_any(result, "Commit creating", False) - mock_log_debug.assert_called_with('Commit creating result', extra={'extra_log_attributes': {'result': expected}}) + mock_log_debug.assert_called_with( + "Commit creating result", extra={"extra_log_attributes": {"result": expected}} + ) def test_get_token_header_or_fail(): @@ -102,6 +106,7 @@ def test_get_token_header_or_fail(): == "Codecov token not found. Please provide Codecov token with -t flag." ) + def test_get_token_header(): # Test with a valid UUID token token = uuid.uuid4() diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 9033082c..68dd840b 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -301,10 +301,14 @@ def test_upload_sender_result_fail_post_400( assert sender.warnings is not None - @pytest.mark.parametrize("error_code", [500, 502]) def test_upload_sender_result_fail_post_500s( - self, mocker, mocked_responses, mocked_legacy_upload_endpoint, capsys, error_code + self, + mocker, + mocked_responses, + mocked_legacy_upload_endpoint, + capsys, + error_code, ): mocker.patch("codecov_cli.helpers.request.sleep") mocked_legacy_upload_endpoint.status = error_code diff --git a/tests/services/empty_upload/test_empty_upload.py b/tests/services/empty_upload/test_empty_upload.py index 16f9f946..b49d055e 100644 --- a/tests/services/empty_upload/test_empty_upload.py +++ b/tests/services/empty_upload/test_empty_upload.py @@ -21,7 +21,14 @@ def test_empty_upload_with_warnings(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False, None + "commit_sha", + "owner/repo", + uuid.uuid4(), + "service", + None, + False, + False, + None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -50,7 +57,14 @@ def test_empty_upload_with_error(mocker): runner = CliRunner() with runner.isolation() as outstreams: res = empty_upload_logic( - "commit_sha", "owner/repo", uuid.uuid4(), "service", None, False, False, None + "commit_sha", + "owner/repo", + uuid.uuid4(), + "service", + None, + False, + False, + None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) diff --git a/tests/services/report/test_report_service.py b/tests/services/report/test_report_service.py index b3a0f04a..ac4c222b 100644 --- a/tests/services/report/test_report_service.py +++ b/tests/services/report/test_report_service.py @@ -124,5 +124,12 @@ def test_create_report_command_with_error(mocker): warnings=[], ) mock_send_report_data.assert_called_with( - "commit_sha", "code", "github", "token", "owner::::repo", "enterprise_url", 1, None + "commit_sha", + "code", + "github", + "token", + "owner::::repo", + "enterprise_url", + 1, + None, ) diff --git a/tests/services/static_analysis/test_static_analysis_service.py b/tests/services/static_analysis/test_static_analysis_service.py index 262a3130..635eecf9 100644 --- a/tests/services/static_analysis/test_static_analysis_service.py +++ b/tests/services/static_analysis/test_static_analysis_service.py @@ -136,7 +136,7 @@ async def side_effect(*args, **kwargs): should_force=False, folders_to_exclude=[], enterprise_url=None, - args=None + args=None, ) mock_file_finder.assert_called_with({}) mock_file_finder.return_value.find_files.assert_called() diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index afe717ff..29261fa1 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -220,12 +220,16 @@ def test_find_coverage_files_with_file_in_parent( for file in coverage_files: file.touch() - coverage_file_finder.explicitly_listed_files = [project_root.parent / "coverage.xml"] + coverage_file_finder.explicitly_listed_files = [ + project_root.parent / "coverage.xml" + ] result = sorted( [file.get_filename() for file in coverage_file_finder.find_files()] ) - expected = [UploadCollectionResultFile(Path(f"{project_root.parent}/coverage.xml"))] + expected = [ + UploadCollectionResultFile(Path(f"{project_root.parent}/coverage.xml")) + ] expected_paths = sorted([file.get_filename() for file in expected]) assert result == expected_paths diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 4bba13e9..081ee2bb 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -84,7 +84,12 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) + mock_select_network_finder.assert_called_with( + versioning_system, + network_filter=None, + network_prefix=None, + network_root_folder=None, + ) mock_generate_upload_data.assert_called_with("coverage") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, @@ -174,7 +179,12 @@ def test_do_upload_logic_happy_path(mocker): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) + mock_select_network_finder.assert_called_with( + versioning_system, + network_filter=None, + network_prefix=None, + network_root_folder=None, + ) mock_generate_upload_data.assert_called_with("coverage") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, @@ -251,7 +261,12 @@ def test_do_upload_logic_dry_run(mocker): ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) + mock_select_network_finder.assert_called_with( + versioning_system, + network_filter=None, + network_prefix=None, + network_root_folder=None, + ) assert mock_generate_upload_data.call_count == 1 assert mock_send_upload_data.call_count == 0 mock_select_preparation_plugins.assert_called_with( @@ -406,7 +421,12 @@ def side_effect(*args, **kwargs): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) + mock_select_network_finder.assert_called_with( + versioning_system, + network_filter=None, + network_prefix=None, + network_root_folder=None, + ) mock_generate_upload_data.assert_called_with("coverage") mock_upload_completion_call.assert_called_with( commit_sha="commit_sha", @@ -480,7 +500,12 @@ def side_effect(*args, **kwargs): cli_config, ["first_plugin", "another", "forth"] ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") - mock_select_network_finder.assert_called_with(versioning_system, network_filter=None, network_prefix=None, network_root_folder=None) + mock_select_network_finder.assert_called_with( + versioning_system, + network_filter=None, + network_prefix=None, + network_root_folder=None, + ) mock_generate_upload_data.assert_called_with("coverage") @@ -537,7 +562,7 @@ def test_do_upload_logic_happy_path_test_results(mocker): pull_request_number="pr", git_service="git_service", enterprise_url=None, - args={"args": "fake_args"} + args={"args": "fake_args"}, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -549,7 +574,12 @@ def test_do_upload_logic_happy_path_test_results(mocker): assert res == UploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_not_called mock_select_file_finder.assert_called_with(None, None, None, False, "test_results") - mock_select_network_finder.assert_called_with(versioning_system, network_filter="some_dir", network_prefix="hello/", network_root_folder="root/") + mock_select_network_finder.assert_called_with( + versioning_system, + network_filter="some_dir", + network_prefix="hello/", + network_root_folder="root/", + ) mock_generate_upload_data.assert_called_with("test_results") mock_send_upload_data.assert_called_with( mock_generate_upload_data.return_value, @@ -569,5 +599,5 @@ def test_do_upload_logic_happy_path_test_results(mocker): "service", "git_service", None, - {"args": "fake_args"} + {"args": "fake_args"}, ) From ab27ada14dd7de6f1dd4fdfb71dccbd4a78f9f06 Mon Sep 17 00:00:00 2001 From: michelletran-codecov <167130096+michelletran-codecov@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:41:45 -0400 Subject: [PATCH 098/128] Aggregate the test results files in CI (#507) --- .github/workflows/ci.yml | 67 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9d26e76..f98b4ce9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,12 +76,18 @@ jobs: pip install -r tests/requirements.txt - name: Test with pytest run: | - pytest --cov --junitxml=junit.xml + pytest --cov --junitxml=${{matrix.python-version}}junit.xml - name: Dogfooding codecov-cli if: ${{ !github.event.pull_request.head.repo.fork && github.repository_owner == 'codecov' }} run: | codecovcli -v do-upload --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} codecovcli do-upload --report-type test_results --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} + - name: Upload artifacts for test-results-processing + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: ${{matrix.python-version}}junit.xml + path: ${{matrix.python-version}}junit.xml static-analysis: runs-on: ubuntu-latest @@ -131,38 +137,31 @@ jobs: run: | codecovcli --codecov-yml-path=codecov.yml do-upload --plugin pycoverage --plugin compress-pycoverage --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --flag smart-labels - test-process-test-results-cmd: + process-test-results: + if: ${{ always() }} + needs: build-test-upload runs-on: ubuntu-latest - permissions: - pull-requests: write - strategy: - fail-fast: false - matrix: - include: - - python-version: "3.12" - - python-version: "3.11" - - python-version: "3.10" - - python-version: "3.9" - - python-version: "3.8" steps: - - uses: actions/checkout@v4 - with: - submodules: true - fetch-depth: 2 - - name: Set up Python ${{matrix.python-version}} - uses: actions/setup-python@v5 - with: - python-version: "${{matrix.python-version}}" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - python -m pip install -e . - pip install -r tests/requirements.txt - - name: Test with pytest - run: | - pytest --cov --junitxml=junit.xml - - name: Dogfooding codecov-cli - if: ${{ !cancelled() }} - run: | - codecovcli process-test-results --provider-token ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 2 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies for Dogfooding + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + python -m pip install -e . + pip install -r tests/requirements.txt + - name: Download all test results + uses: actions/download-artifact@v4 + with: + pattern: "*junit.xml" + path: "test_results" + merge-multiple: true + - name: Dogfooding codecov-cli + if: ${{ !cancelled() }} + run: | + codecovcli process-test-results --provider-token ${{ secrets.GITHUB_TOKEN }} --dir test_results From f4c17e1996d1a06db7c1c135f82b9be2aa7a85f1 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:53:44 -0500 Subject: [PATCH 099/128] fix: make versionc all in tests dynamic (#517) --- tests/commands/test_process_test_results.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index 97c6d956..1baf73b2 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -3,6 +3,7 @@ from click.testing import CliRunner +from codecov_cli import __version__ from codecov_cli.main import cli from codecov_cli.types import RequestResult @@ -53,7 +54,7 @@ def test_process_test_results( "codecov_yml_path": None, "enterprise_url": None, "verbose": False, - "version": "cli-0.7.4", + "version": f"cli-{__version__}", "command": "process-test-results", "provider_token": "whatever", "disable_search": True, From b63a8a101bdb78957bbce43976a8d0cdc1b42bc8 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Tue, 1 Oct 2024 11:00:14 -0400 Subject: [PATCH 100/128] Prepare release 0.7.5 (#516) Co-authored-by: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bb92b319..2102345e 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.4", + version="0.7.5", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 5fb28a6fc3273bd8b754dd734855fe84b1e2dd1d Mon Sep 17 00:00:00 2001 From: michelletran-codecov <167130096+michelletran-codecov@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:20:48 -0400 Subject: [PATCH 101/128] Only run test results parser for PRs (#512) We don't want this to run when merging. So, check that the Github Ref is available before running the command. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f98b4ce9..be24dc08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: codecovcli -v do-upload --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} codecovcli do-upload --report-type test_results --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} --plugin pycoverage --flag python${{matrix.python-version}} - name: Upload artifacts for test-results-processing - if: ${{ always() }} + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: ${{matrix.python-version}}junit.xml @@ -162,6 +162,6 @@ jobs: path: "test_results" merge-multiple: true - name: Dogfooding codecov-cli - if: ${{ !cancelled() }} + if: ${{ !cancelled() && github.ref }} run: | codecovcli process-test-results --provider-token ${{ secrets.GITHUB_TOKEN }} --dir test_results From a22eaf077bee6a3031d59879d3b7b75267500103 Mon Sep 17 00:00:00 2001 From: michelletran-codecov <167130096+michelletran-codecov@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:22:15 -0400 Subject: [PATCH 102/128] Update test-results-parser (#524) * Update test-results-parser This will include the new formatted comment * Use local version of codecov-cli for label analysis Since the tests are running on the "official" version of the CLI, it is failing for any interface changes between the official version and the version currently in review. So, changing it to also use the local CLI for running the tests. --- .github/workflows/ci.yml | 8 ++++--- codecov_cli/commands/process_test_results.py | 2 +- requirements.txt | 4 ++-- setup.py | 2 +- tests/commands/test_process_test_results.py | 24 +------------------- 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be24dc08..0076094a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,10 +124,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Install CLI + - name: Install dependencies for Dogfooding run: | - pip install -r requirements.txt -r tests/requirements.txt - pip install codecov-cli + python -m pip install --upgrade pip + pip install -r requirements.txt + python -m pip install -e . + pip install -r tests/requirements.txt - name: Label Analysis run: | BASE_SHA=$(git merge-base HEAD^ origin/main) diff --git a/codecov_cli/commands/process_test_results.py b/codecov_cli/commands/process_test_results.py index f887c5b9..d2ab1b9d 100644 --- a/codecov_cli/commands/process_test_results.py +++ b/codecov_cli/commands/process_test_results.py @@ -173,7 +173,7 @@ def generate_message_payload(upload_collection_results): try: logger.info(f"Parsing {result.get_filename()}") testruns = parse_junit_xml(result.get_content()) - for testrun in testruns: + for testrun in testruns.testruns: if ( testrun.outcome == Outcome.Failure or testrun.outcome == Outcome.Error diff --git a/requirements.txt b/requirements.txt index d40822af..781e4223 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile setup.py @@ -43,7 +43,7 @@ sniffio==1.3.0 # anyio # httpcore # httpx -test-results-parser==0.1.0 +test-results-parser==0.5.1 # via codecov-cli (setup.py) tree-sitter==0.20.2 # via codecov-cli (setup.py) diff --git a/setup.py b/setup.py index 2102345e..e3e6a86f 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ "pyyaml==6.*", "responses==0.21.*", "tree-sitter==0.20.*", - "test-results-parser==0.1.*", + "test-results-parser==0.5.*", "regex", ], entry_points={ diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index 1baf73b2..2a6a0575 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -45,29 +45,7 @@ def test_process_test_results( assert result.exit_code == 0 - mocked_post.assert_called_with( - url="https://api.github.com/repos/fake/repo/issues/pull/comments", - data={ - "body": "### :x: Failed Test Results: \nCompleted 4 tests with **`1 failed`**, 3 passed and 0 skipped.\n
View the full list of failed tests\n\n| **Test Description** | **Failure message** |\n| :-- | :-- |\n|
Testsuite:
api.temp.calculator.test_calculator::test_divide

Test name:
pytest
|
def
test_divide():
&gt; assert Calculator.divide(1, 2) == 0.5
E assert 1.0 == 0.5
E + where 1.0 = &lt;function Calculator.divide at 0x104c9eb90&gt;(1, 2)
E + where &lt;function Calculator.divide at 0x104c9eb90&gt; = Calculator.divide
.../temp/calculator/test_calculator.py:30: AssertionError
|", - "cli_args": { - "auto_load_params_from": None, - "codecov_yml_path": None, - "enterprise_url": None, - "verbose": False, - "version": f"cli-{__version__}", - "command": "process-test-results", - "provider_token": "whatever", - "disable_search": True, - "dir": os.getcwd(), - "exclude_folders": (), - }, - }, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": "Bearer whatever", - "X-GitHub-Api-Version": "2022-11-28", - }, - ) + mocked_post.assert_called_once() def test_process_test_results_non_existent_file(mocker, tmpdir): From 372aa00af031bf3faed2ee898cd958d395729f6a Mon Sep 17 00:00:00 2001 From: Rohan Bhaumik Date: Thu, 10 Oct 2024 11:55:37 -0400 Subject: [PATCH 103/128] Update README.md (#525) Better formatting of options for the `empty-upload` command --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1a09683b..457a2307 100644 --- a/README.md +++ b/README.md @@ -240,14 +240,14 @@ are ignored by codecov (including README and configuration files) `Usage: codecovcli empty-upload [OPTIONS]` -Options: - -C, --sha, --commit-sha TEXT Commit SHA (with 40 chars) [required] - -Z, --fail-on-error Exit with non-zero code in case of error - --git-service [github|gitlab|bitbucket|github_enterprise|gitlab_enterprise|bitbucket_server] - -t, --token TEXT Codecov upload token - -r, --slug TEXT owner/repo slug used instead of the private - repo token in Self-hosted - -h, --help Show this message and exit. +| Options | Description | usage +| :---: | :---: | :---: | + | -C, --sha, --commit-sha TEXT |Commit SHA (with 40 chars) | Required + | -t, --token TEXT |Codecov upload token |Required +| -r, --slug TEXT | owner/repo slug used instead of the private repo token in Self-hosted | Optional +| -Z, --fail-on-error | Exit with non-zero code in case of error|Optional +| --git-service| Options: github, gitlab, bitbucket, github_enterprise, gitlab_enterprise, bitbucket_server | Optional +| -h, --help | Show this message and exit.|Optional # How to Use Local Upload From 5fb183704854873f94a8ba1df4956f3350b03362 Mon Sep 17 00:00:00 2001 From: michelletran-codecov <167130096+michelletran-codecov@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:46:33 -0400 Subject: [PATCH 104/128] Rewrite to the same comment for offline test analytics (#522) * Ensure that we run offline test analytics on PRs The ref can also reference a branch when it's merged. Ensure that we're only running this for a PR. * Add ability to update existing GitHub Actions comment * Remove writing to GitHub Actions Summary To have feature parity with the existing Test Analytics, we don't actually want to publish to the GitHub Actions Summary. --- .github/workflows/ci.yml | 5 +- codecov_cli/commands/process_test_results.py | 148 ++++++++++---- codecov_cli/helpers/request.py | 7 + requirements.txt | 2 +- tests/commands/test_process_test_results.py | 195 +++++++++++++++---- 5 files changed, 277 insertions(+), 80 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0076094a..2e96fda3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,7 +163,8 @@ jobs: pattern: "*junit.xml" path: "test_results" merge-multiple: true + - name: Dogfooding codecov-cli - if: ${{ !cancelled() && github.ref }} + if: ${{ !cancelled() && github.ref && contains(github.ref, 'pull') }} run: | - codecovcli process-test-results --provider-token ${{ secrets.GITHUB_TOKEN }} --dir test_results + codecovcli process-test-results --dir test_results --github-token ${{ secrets.GITHUB_TOKEN }} diff --git a/codecov_cli/commands/process_test_results.py b/codecov_cli/commands/process_test_results.py index d2ab1b9d..6d3d89f2 100644 --- a/codecov_cli/commands/process_test_results.py +++ b/codecov_cli/commands/process_test_results.py @@ -1,8 +1,9 @@ +import json import logging import os import pathlib from dataclasses import dataclass -from typing import List +from typing import Any, Dict, List, Optional import click from test_results_parser import ( @@ -16,13 +17,17 @@ from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.request import ( log_warnings_and_errors_if_any, + send_get_request, send_post_request, ) from codecov_cli.services.upload.file_finder import select_file_finder -from codecov_cli.types import CommandContext +from codecov_cli.types import CommandContext, RequestResult, UploadCollectionResultFile logger = logging.getLogger("codecovcli") +# Search marker so that we can find the comment when looking for previously created comments +CODECOV_SEARCH_MARKER = "" + _process_test_results_options = [ click.option( @@ -61,8 +66,8 @@ default=False, ), click.option( - "--provider-token", - help="Token used to make calls to Repo provider API", + "--github-token", + help="If specified, output the message to the specified GitHub PR.", type=str, default=None, ), @@ -92,65 +97,133 @@ def process_test_results( files=None, exclude_folders=None, disable_search=None, - provider_token=None, + github_token=None, ): - if provider_token is None: - raise click.ClickException( - "Provider token was not provided. Make sure to pass --provider-token option with the contents of the GITHUB_TOKEN secret, so we can make a comment." - ) + file_finder = select_file_finder( + dir, exclude_folders, files, disable_search, report_type="test_results" + ) - summary_file_path = os.getenv("GITHUB_STEP_SUMMARY") - if summary_file_path is None: + upload_collection_results: List[ + UploadCollectionResultFile + ] = file_finder.find_files() + if len(upload_collection_results) == 0: raise click.ClickException( - "Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable." + "No JUnit XML files were found. Make sure to specify them using the --file option." ) + payload: TestResultsNotificationPayload = generate_message_payload( + upload_collection_results + ) + + message: str = f"{build_message(payload)} {CODECOV_SEARCH_MARKER}" + + args: Dict[str, str] = get_cli_args(ctx) + + maybe_write_to_github_action(message, github_token, args) + + click.echo(message) + + +def maybe_write_to_github_action( + message: str, github_token: str, args: Dict[str, str] +) -> None: + if github_token is None: + # If no token is passed, then we will assume users are not running in a GitHub Action + return + + maybe_write_to_github_comment(message, github_token, args) + + +def maybe_write_to_github_comment( + message: str, github_token: str, args: Dict[str, str] +) -> None: slug = os.getenv("GITHUB_REPOSITORY") if slug is None: raise click.ClickException( - "Error getting repo slug from environment. Can't find GITHUB_REPOSITORY environment variable." + "Error getting repo slug from environment. " + "Can't find GITHUB_REPOSITORY environment variable." ) ref = os.getenv("GITHUB_REF") if ref is None or "pull" not in ref: raise click.ClickException( - "Error getting PR number from environment. Can't find GITHUB_REF environment variable." + "Error getting PR number from environment. " + "Can't find GITHUB_REF environment variable." ) + # GITHUB_REF is documented here: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + pr_number = ref.split("/")[2] - file_finder = select_file_finder( - dir, exclude_folders, files, disable_search, report_type="test_results" + existing_comment = find_existing_github_comment(github_token, slug, pr_number) + comment_id = None + if existing_comment is not None: + comment_id = existing_comment.get("id") + + create_or_update_github_comment( + github_token, slug, pr_number, message, comment_id, args ) - upload_collection_results = file_finder.find_files() - if len(upload_collection_results) == 0: - raise click.ClickException( - "No JUnit XML files were found. Make sure to specify them using the --file option." - ) - payload = generate_message_payload(upload_collection_results) +def find_existing_github_comment( + github_token: str, repo_slug: str, pr_number: int +) -> Optional[Dict[str, Any]]: + url = f"https://api.github.com/repos/{repo_slug}/issues/{pr_number}/comments" - message = build_message(payload) + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {github_token}", + "X-GitHub-Api-Version": "2022-11-28", + } + page = 1 - # write to step summary file - with open(summary_file_path, "w") as f: - f.write(message) + results = get_github_response_or_error(url, headers, page) + while results != []: + for comment in results: + comment_user = comment.get("user") + if ( + CODECOV_SEARCH_MARKER in comment.get("body", "") + and comment_user + and comment_user.get("login", "") == "github-actions[bot]" + ): + return comment - # GITHUB_REF is documented here: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables - pr_number = ref.split("/")[2] + page += 1 + results = get_github_response_or_error(url, headers, page) - args = get_cli_args(ctx) - create_github_comment(provider_token, slug, pr_number, message, args) + # No matches, return None + return None -def create_github_comment(token, repo_slug, pr_number, message, args): - url = f"https://api.github.com/repos/{repo_slug}/issues/{pr_number}/comments" +def get_github_response_or_error( + url: str, headers: Dict[str, str], page: int +) -> Dict[str, Any]: + request_results: RequestResult = send_get_request( + url, headers, params={"page": page} + ) + if request_results.status_code != 200: + raise click.ClickException("Cannot find existing GitHub comment for PR.") + results = json.loads(request_results.text) + return results + + +def create_or_update_github_comment( + token: str, + repo_slug: str, + pr_number: str, + message: str, + comment_id: Optional[str], + args: Dict[str, Any], +) -> None: + if comment_id is not None: + url = f"https://api.github.com/repos/{repo_slug}/issues/comments/{comment_id}" + else: + url = f"https://api.github.com/repos/{repo_slug}/issues/{pr_number}/comments" headers = { "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", "X-GitHub-Api-Version": "2022-11-28", } - logger.info("Posting github comment") + logger.info(f"Posting GitHub comment {comment_id}") log_warnings_and_errors_if_any( send_post_request( @@ -165,15 +238,16 @@ def create_github_comment(token, repo_slug, pr_number, message, args): ) -def generate_message_payload(upload_collection_results): +def generate_message_payload( + upload_collection_results: List[UploadCollectionResultFile], +) -> TestResultsNotificationPayload: payload = TestResultsNotificationPayload(failures=[]) for result in upload_collection_results: - testruns = [] try: logger.info(f"Parsing {result.get_filename()}") - testruns = parse_junit_xml(result.get_content()) - for testrun in testruns.testruns: + parsed_info = parse_junit_xml(result.get_content()) + for testrun in parsed_info.testruns: if ( testrun.outcome == Outcome.Failure or testrun.outcome == Outcome.Error diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index dbafc9a3..6c153bd5 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -95,6 +95,13 @@ def send_post_request( return request_result(post(url=url, data=data, headers=headers, params=params)) +@retry_request +def send_get_request( + url: str, headers: dict = None, params: dict = None +) -> RequestResult: + return request_result(get(url=url, headers=headers, params=params)) + + def get_token_header_or_fail(token: str) -> dict: if token is None: raise click.ClickException( diff --git a/requirements.txt b/requirements.txt index 781e4223..2d33a3d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile setup.py diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index 2a6a0575..e583159d 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -1,9 +1,8 @@ -import logging +import json import os from click.testing import CliRunner -from codecov_cli import __version__ from codecov_cli.main import cli from codecov_cli.types import RequestResult @@ -20,7 +19,6 @@ def test_process_test_results( { "GITHUB_REPOSITORY": "fake/repo", "GITHUB_REF": "pull/fake/pull", - "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, }, ) mocked_post = mocker.patch( @@ -34,8 +32,6 @@ def test_process_test_results( cli, [ "process-test-results", - "--provider-token", - "whatever", "--file", "samples/junit.xml", "--disable-search", @@ -44,21 +40,100 @@ def test_process_test_results( ) assert result.exit_code == 0 + # Ensure that there's an output + assert result.output - mocked_post.assert_called_once() +def test_process_test_results_create_github_message( + mocker, + tmpdir, +): + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_REF": "pull/fake/123", + }, + ) + + mocker.patch( + "codecov_cli.commands.process_test_results.send_get_request", + return_value=RequestResult(status_code=200, error=None, warnings=[], text="[]"), + ) + + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--github-token", + "fake-token", + "--file", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 0 + assert ( + mocked_post.call_args.kwargs["url"] + == "https://api.github.com/repos/fake/repo/issues/123/comments" + ) + + +def test_process_test_results_update_github_message( + mocker, + tmpdir, +): -def test_process_test_results_non_existent_file(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, { "GITHUB_REPOSITORY": "fake/repo", - "GITHUB_REF": "pull/fake/pull", - "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, + "GITHUB_REF": "pull/fake/123", }, ) + + github_fake_comments1 = [ + {"id": 54321, "user": {"login": "fake"}, "body": "some text"}, + ] + github_fake_comments2 = [ + { + "id": 12345, + "user": {"login": "github-actions[bot]"}, + "body": " and some other fake body", + }, + ] + + mocker.patch( + "codecov_cli.commands.process_test_results.send_get_request", + side_effect=[ + RequestResult( + status_code=200, + error=None, + warnings=[], + text=json.dumps(github_fake_comments1), + ), + RequestResult( + status_code=200, + error=None, + warnings=[], + text=json.dumps(github_fake_comments2), + ), + ], + ) + mocked_post = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( @@ -70,36 +145,80 @@ def test_process_test_results_non_existent_file(mocker, tmpdir): cli, [ "process-test-results", - "--provider-token", - "whatever", + "--github-token", + "fake-token", "--file", - "samples/fake.xml", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 0 + assert ( + mocked_post.call_args.kwargs["url"] + == "https://api.github.com/repos/fake/repo/issues/comments/12345" + ) + + +def test_process_test_results_errors_getting_comments( + mocker, + tmpdir, +): + + tmp_file = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_REF": "pull/fake/123", + }, + ) + + mocker.patch( + "codecov_cli.commands.process_test_results.send_get_request", + return_value=RequestResult( + status_code=400, + error=None, + warnings=[], + text="", + ), + ) + + mocked_post = mocker.patch( + "codecov_cli.commands.process_test_results.send_post_request", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text="yay it worked" + ), + ) + runner = CliRunner() + result = runner.invoke( + cli, + [ + "process-test-results", + "--github-token", + "fake-token", + "--file", + "samples/junit.xml", "--disable-search", ], obj={}, ) assert result.exit_code == 1 - expected_logs = [ - "ci service found", - "Some files were not found", - ] - for log in expected_logs: - assert log in result.output -def test_process_test_results_missing_repo(mocker, tmpdir): +def test_process_test_results_non_existent_file(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, { + "GITHUB_REPOSITORY": "fake/repo", "GITHUB_REF": "pull/fake/pull", - "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, }, ) - if "GITHUB_REPOSITORY" in os.environ: - del os.environ["GITHUB_REPOSITORY"] mocked_post = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( @@ -111,10 +230,8 @@ def test_process_test_results_missing_repo(mocker, tmpdir): cli, [ "process-test-results", - "--provider-token", - "whatever", "--file", - "samples/junit.xml", + "samples/fake.xml", "--disable-search", ], obj={}, @@ -123,25 +240,23 @@ def test_process_test_results_missing_repo(mocker, tmpdir): assert result.exit_code == 1 expected_logs = [ "ci service found", - "Error: Error getting repo slug from environment. Can't find GITHUB_REPOSITORY environment variable.", + "Some files were not found", ] for log in expected_logs: assert log in result.output -def test_process_test_results_missing_ref(mocker, tmpdir): +def test_process_test_results_missing_repo(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, { - "GITHUB_REPOSITORY": "fake/repo", - "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, + "GITHUB_REF": "pull/fake/pull", }, ) - - if "GITHUB_REF" in os.environ: - del os.environ["GITHUB_REF"] + if "GITHUB_REPOSITORY" in os.environ: + del os.environ["GITHUB_REPOSITORY"] mocked_post = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( @@ -153,7 +268,7 @@ def test_process_test_results_missing_ref(mocker, tmpdir): cli, [ "process-test-results", - "--provider-token", + "--github-token", "whatever", "--file", "samples/junit.xml", @@ -165,24 +280,24 @@ def test_process_test_results_missing_ref(mocker, tmpdir): assert result.exit_code == 1 expected_logs = [ "ci service found", - "Error: Error getting PR number from environment. Can't find GITHUB_REF environment variable.", + "Error: Error getting repo slug from environment. Can't find GITHUB_REPOSITORY environment variable.", ] for log in expected_logs: assert log in result.output -def test_process_test_results_missing_step_summary(mocker, tmpdir): +def test_process_test_results_missing_ref(mocker, tmpdir): tmp_file = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, { "GITHUB_REPOSITORY": "fake/repo", - "GITHUB_REF": "pull/fake/pull", }, ) - if "GITHUB_STEP_SUMMARY" in os.environ: - del os.environ["GITHUB_STEP_SUMMARY"] + + if "GITHUB_REF" in os.environ: + del os.environ["GITHUB_REF"] mocked_post = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( @@ -194,7 +309,7 @@ def test_process_test_results_missing_step_summary(mocker, tmpdir): cli, [ "process-test-results", - "--provider-token", + "--github-token", "whatever", "--file", "samples/junit.xml", @@ -206,7 +321,7 @@ def test_process_test_results_missing_step_summary(mocker, tmpdir): assert result.exit_code == 1 expected_logs = [ "ci service found", - "Error: Error getting step summary file path from environment. Can't find GITHUB_STEP_SUMMARY environment variable.", + "Error: Error getting PR number from environment. Can't find GITHUB_REF environment variable.", ] for log in expected_logs: assert log in result.output From 0f2ea9ce52305af4ede7647c1ad77a8bdf322880 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Fri, 11 Oct 2024 13:50:43 -0400 Subject: [PATCH 105/128] Prepare release 0.7.6 (#526) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e3e6a86f..f9e604bc 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.5", + version="0.7.6", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 6efe50c4d4c4b51a9c7799e48c1042288ba64378 Mon Sep 17 00:00:00 2001 From: Trent Schmidt Date: Fri, 11 Oct 2024 14:39:34 -0400 Subject: [PATCH 106/128] Adding small ci job (#527) --- .github/workflows/ci-job.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/ci-job.yml diff --git a/.github/workflows/ci-job.yml b/.github/workflows/ci-job.yml new file mode 100644 index 00000000..2379156c --- /dev/null +++ b/.github/workflows/ci-job.yml @@ -0,0 +1,41 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: CLI CI Job + +on: + pull_request: + push: + branches: + - main + +jobs: + build-test-upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 2 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + python -m pip install -e . + pip install -r tests/requirements.txt + - name: Test with pytest + run: | + pytest --cov --junitxml=3.12junit.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + flags: python3.12 + fail_ci_if_error: true + verbose: true From 3333bcbb47d71525df657eebd20b72522d552aa3 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Thu, 17 Oct 2024 08:37:48 -0500 Subject: [PATCH 107/128] feat: add gcov capabilities (#536) * first pass * fix: update tests --- codecov_cli/commands/upload.py | 24 ++++++ codecov_cli/commands/upload_process.py | 74 ++++++++++--------- codecov_cli/plugins/__init__.py | 22 ++++-- codecov_cli/plugins/gcov.py | 24 +++--- codecov_cli/services/upload/__init__.py | 22 +++++- .../services/upload/upload_collector.py | 2 + tests/commands/test_invoke_upload_process.py | 4 + tests/plugins/test_instantiation.py | 19 +++-- .../services/upload/test_upload_collector.py | 14 ++-- tests/services/upload/test_upload_service.py | 64 +++++++++++----- 10 files changed, 187 insertions(+), 82 deletions(-) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index dfbfe431..e191fae0 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -178,6 +178,22 @@ def _turn_env_vars_into_dict(ctx, params, value): "--network-prefix", help="Specify a prefix on files listed in the network section of the Codecov report. Useful to help resolve path fixing", ), + click.option( + "--gcov-args", + help="Extra arguments to pass to gcov", + ), + click.option( + "--gcov-ignore", + help="Paths to ignore during gcov gathering", + ), + click.option( + "--gcov-include", + help="Paths to include during gcov gathering", + ), + click.option( + "--gcov-executable", + help="gcov executable to run. Defaults to 'gcov'", + ), ] @@ -207,6 +223,10 @@ def do_upload( files_search_explicitly_listed_files: typing.List[pathlib.Path], files_search_root_folder: pathlib.Path, flags: typing.List[str], + gcov_args: typing.Optional[str], + gcov_executable: typing.Optional[str], + gcov_ignore: typing.Optional[str], + gcov_include: typing.Optional[str], git_service: typing.Optional[str], handle_no_reports_found: bool, job_code: typing.Optional[str], @@ -251,6 +271,10 @@ def do_upload( files_search_explicitly_listed_files=list(files_search_explicitly_listed_files), files_search_root_folder=files_search_root_folder, flags=flags, + gcov_args=gcov_args, + gcov_executable=gcov_executable, + gcov_ignore=gcov_ignore, + gcov_include=gcov_include, git_service=git_service, handle_no_reports_found=handle_no_reports_found, job_code=job_code, diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index e6efecc2..b30cdb27 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -25,34 +25,38 @@ @click.pass_context def upload_process( ctx: CommandContext, - commit_sha: str, - report_code: str, + branch: typing.Optional[str], build_code: typing.Optional[str], build_url: typing.Optional[str], - job_code: typing.Optional[str], + commit_sha: str, + disable_file_fixes: bool, + disable_search: bool, + dry_run: bool, env_vars: typing.Dict[str, str], + fail_on_error: bool, + files_search_exclude_folders: typing.List[pathlib.Path], + files_search_explicitly_listed_files: typing.List[pathlib.Path], + files_search_root_folder: pathlib.Path, flags: typing.List[str], + gcov_args: typing.Optional[str], + gcov_executable: typing.Optional[str], + gcov_ignore: typing.Optional[str], + gcov_include: typing.Optional[str], + git_service: typing.Optional[str], + handle_no_reports_found: bool, + job_code: typing.Optional[str], name: typing.Optional[str], network_filter: typing.Optional[str], network_prefix: typing.Optional[str], network_root_folder: pathlib.Path, - files_search_root_folder: pathlib.Path, - files_search_exclude_folders: typing.List[pathlib.Path], - files_search_explicitly_listed_files: typing.List[pathlib.Path], - disable_search: bool, - disable_file_fixes: bool, - token: typing.Optional[str], + parent_sha: typing.Optional[str], plugin_names: typing.List[str], - branch: typing.Optional[str], - slug: typing.Optional[str], pull_request_number: typing.Optional[str], - use_legacy_uploader: bool, - fail_on_error: bool, - dry_run: bool, - git_service: typing.Optional[str], - parent_sha: typing.Optional[str], - handle_no_reports_found: bool, + report_code: str, report_type: str, + slug: typing.Optional[str], + token: typing.Optional[str], + use_legacy_uploader: bool, ): args = get_cli_args(ctx) logger.debug( @@ -85,31 +89,35 @@ def upload_process( ) ctx.invoke( do_upload, - commit_sha=commit_sha, - report_code=report_code, + branch=branch, build_code=build_code, build_url=build_url, - job_code=job_code, + commit_sha=commit_sha, + disable_file_fixes=disable_file_fixes, + disable_search=disable_search, + dry_run=dry_run, env_vars=env_vars, + fail_on_error=fail_on_error, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, flags=flags, + gcov_args=gcov_args, + gcov_executable=gcov_executable, + gcov_ignore=gcov_ignore, + gcov_include=gcov_include, + git_service=git_service, + handle_no_reports_found=handle_no_reports_found, + job_code=job_code, name=name, network_filter=network_filter, network_prefix=network_prefix, network_root_folder=network_root_folder, - files_search_root_folder=files_search_root_folder, - files_search_exclude_folders=files_search_exclude_folders, - files_search_explicitly_listed_files=files_search_explicitly_listed_files, - disable_search=disable_search, - token=token, plugin_names=plugin_names, - branch=branch, - slug=slug, pull_request_number=pull_request_number, - use_legacy_uploader=use_legacy_uploader, - fail_on_error=fail_on_error, - dry_run=dry_run, - git_service=git_service, - handle_no_reports_found=handle_no_reports_found, - disable_file_fixes=disable_file_fixes, + report_code=report_code, report_type=report_type, + slug=slug, + token=token, + use_legacy_uploader=use_legacy_uploader, ) diff --git a/codecov_cli/plugins/__init__.py b/codecov_cli/plugins/__init__.py index 2ce8cb6d..df17c8ab 100644 --- a/codecov_cli/plugins/__init__.py +++ b/codecov_cli/plugins/__init__.py @@ -17,12 +17,17 @@ def run_preparation(self, collector): pass -def select_preparation_plugins(cli_config: typing.Dict, plugin_names: typing.List[str]): - plugins = [_get_plugin(cli_config, p) for p in plugin_names] +def select_preparation_plugins( + cli_config: typing.Dict, plugin_names: typing.List[str], plugin_config: typing.Dict +): + plugins = [_get_plugin(cli_config, p, plugin_config) for p in plugin_names] logger.debug( "Selected preparation plugins", extra=dict( - extra_log_attributes=dict(selected_plugins=list(map(type, plugins))) + extra_log_attributes=dict( + selected_plugins=list(map(type, plugins)), + cli_config=cli_config, + ) ), ) return plugins @@ -59,11 +64,18 @@ def _load_plugin_from_yaml(plugin_dict: typing.Dict): return NoopPlugin() -def _get_plugin(cli_config, plugin_name): +def _get_plugin(cli_config, plugin_name, plugin_config): if plugin_name == "noop": return NoopPlugin() if plugin_name == "gcov": - return GcovPlugin() + return GcovPlugin( + plugin_config.get("project_root", None), + plugin_config.get("folders_to_ignore", None), + plugin_config.get("gcov_executable", "gcov"), + plugin_config.get("gcov_include", None), + plugin_config.get("gcov_ignore", None), + plugin_config.get("gcov_args", None), + ) if plugin_name == "pycoverage": config = cli_config.get("plugins", {}).get("pycoverage", {}) return Pycoverage(config) diff --git a/codecov_cli/plugins/gcov.py b/codecov_cli/plugins/gcov.py index d807ab6b..668095b2 100644 --- a/codecov_cli/plugins/gcov.py +++ b/codecov_cli/plugins/gcov.py @@ -15,24 +15,26 @@ class GcovPlugin(object): def __init__( self, project_root: typing.Optional[pathlib.Path] = None, + folders_to_ignore: typing.Optional[typing.List[str]] = None, + executable: typing.Optional[str] = "gcov", patterns_to_include: typing.Optional[typing.List[str]] = None, patterns_to_ignore: typing.Optional[typing.List[str]] = None, - folders_to_ignore: typing.Optional[typing.List[str]] = None, extra_arguments: typing.Optional[typing.List[str]] = None, ): - self.project_root = project_root or pathlib.Path(os.getcwd()) - self.patterns_to_include = patterns_to_include or [] - self.patterns_to_ignore = patterns_to_ignore or [] - self.folders_to_ignore = folders_to_ignore or [] + self.executable = executable or "gcov" self.extra_arguments = extra_arguments or [] + self.folders_to_ignore = folders_to_ignore or [] + self.patterns_to_ignore = patterns_to_ignore or [] + self.patterns_to_include = patterns_to_include or [] + self.project_root = project_root or pathlib.Path(os.getcwd()) def run_preparation(self, collector) -> PreparationPluginReturn: logger.debug( - "Running gcov plugin...", + f"Running {self.executable} plugin...", ) - if shutil.which("gcov") is None: - logger.warning("gcov is not installed or can't be found.") + if shutil.which(self.executable) is None: + logger.warning(f"{self.executable} is not installed or can't be found.") return filename_include_regex = globs_to_regex(["*.gcno", *self.patterns_to_include]) @@ -49,15 +51,15 @@ def run_preparation(self, collector) -> PreparationPluginReturn: ] if not matched_paths: - logger.warning("No gcov data found.") + logger.warning(f"No {self.executable} data found.") return - logger.warning("Running gcov on the following list of files:") + logger.warning(f"Running {self.executable} on the following list of files:") for path in matched_paths: logger.warning(path) s = subprocess.run( - ["gcov", "-pb", *self.extra_arguments, *matched_paths], + [self.executable, "-pb", *self.extra_arguments, *matched_paths], cwd=self.project_root, capture_output=True, ) diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index 411c820c..c9f2b779 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -40,6 +40,10 @@ def do_upload_logic( files_search_explicitly_listed_files: typing.List[Path], files_search_root_folder: Path, flags: typing.List[str], + gcov_args: typing.Optional[str], + gcov_executable: typing.Optional[str], + gcov_ignore: typing.Optional[str], + gcov_include: typing.Optional[str], git_service: typing.Optional[str], handle_no_reports_found: bool = False, job_code: typing.Optional[str], @@ -55,8 +59,18 @@ def do_upload_logic( upload_file_type: str = "coverage", use_legacy_uploader: bool = False, ): + plugin_config = { + "folders_to_ignore": files_search_exclude_folders, + "gcov_args": gcov_args, + "gcov_executable": gcov_executable, + "gcov_ignore": gcov_ignore, + "gcov_include": gcov_include, + "project_root": files_search_root_folder, + } if upload_file_type == "coverage": - preparation_plugins = select_preparation_plugins(cli_config, plugin_names) + preparation_plugins = select_preparation_plugins( + cli_config, plugin_names, plugin_config + ) elif upload_file_type == "test_results": preparation_plugins = [] file_selector = select_file_finder( @@ -73,7 +87,11 @@ def do_upload_logic( network_root_folder=network_root_folder, ) collector = UploadCollector( - preparation_plugins, network_finder, file_selector, disable_file_fixes + preparation_plugins, + network_finder, + file_selector, + disable_file_fixes, + plugin_config, ) try: upload_data = collector.generate_upload_data(upload_file_type) diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 47a99670..53dcd860 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -29,12 +29,14 @@ def __init__( preparation_plugins: typing.List[PreparationPluginInterface], network_finder: NetworkFinder, file_finder: FileFinder, + plugin_config: dict, disable_file_fixes: bool = False, ): self.preparation_plugins = preparation_plugins self.network_finder = network_finder self.file_finder = file_finder self.disable_file_fixes = disable_file_fixes + self.plugin_config = plugin_config def _produce_file_fixes( self, files: typing.List[str] diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index ec3981b2..8f6fcbdc 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -126,6 +126,10 @@ def test_upload_process_options(mocker): " --network-prefix TEXT Specify a prefix on files listed in the", " network section of the Codecov report. Useful", " to help resolve path fixing", + " --gcov-args TEXT Extra arguments to pass to gcov", + " --gcov-ignore TEXT Paths to ignore during gcov gathering", + " --gcov-include TEXT Paths to include during gcov gathering", + " --gcov-executable TEXT gcov executable to run. Defaults to 'gcov'", " --parent-sha TEXT SHA (with 40 chars) of what should be the", " parent of this commit", " -h, --help Show this message and exit.", diff --git a/tests/plugins/test_instantiation.py b/tests/plugins/test_instantiation.py index caaa3516..97e5b68a 100644 --- a/tests/plugins/test_instantiation.py +++ b/tests/plugins/test_instantiation.py @@ -106,40 +106,46 @@ def __init__(self): def test_get_plugin_gcov(): - res = _get_plugin({}, "gcov") + res = _get_plugin({}, "gcov", {}) + assert isinstance(res, GcovPlugin) + + res = _get_plugin({}, "gcov", { + 'gcov_executable': 'lcov', + }) assert isinstance(res, GcovPlugin) def test_get_plugin_xcode(): - res = _get_plugin({}, "xcode") + res = _get_plugin({}, "xcode", {}) assert isinstance(res, XcodePlugin) def test_get_plugin_noop(): - res = _get_plugin({}, "noop") + res = _get_plugin({}, "noop", {}) assert isinstance(res, NoopPlugin) def test_get_plugin_pycoverage(): - res = _get_plugin({}, "pycoverage") + res = _get_plugin({}, "pycoverage", {}) assert isinstance(res, Pycoverage) assert res.config == PycoverageConfig() assert res.config.report_type == "xml" pycoverage_config = {"project_root": "project/root", "report_type": "json"} - res = _get_plugin({"plugins": {"pycoverage": pycoverage_config}}, "pycoverage") + res = _get_plugin({"plugins": {"pycoverage": pycoverage_config}}, "pycoverage", {}) assert isinstance(res, Pycoverage) assert res.config == PycoverageConfig(pycoverage_config) assert res.config.report_type == "json" def test_get_plugin_compress_pycoverage(): - res = _get_plugin({}, "compress-pycoverage") + res = _get_plugin({}, "compress-pycoverage", {}) assert isinstance(res, CompressPycoverageContexts) res = _get_plugin( {"plugins": {"compress-pycoverage": {"file_to_compress": "something.json"}}}, "compress-pycoverage", + {}, ) assert isinstance(res, CompressPycoverageContexts) assert str(res.file_to_compress) == "something.json" @@ -180,6 +186,7 @@ def __init__(self, banana=None): } }, ["gcov", "something", "otherthing", "second", "lalalala"], + {} ) assert len(res) == 5 assert isinstance(res[0], GcovPlugin) diff --git a/tests/services/upload/test_upload_collector.py b/tests/services/upload/test_upload_collector.py index a433b904..39124d0e 100644 --- a/tests/services/upload/test_upload_collector.py +++ b/tests/services/upload/test_upload_collector.py @@ -11,7 +11,7 @@ def test_fix_kt_files(): kt_file = Path("tests/data/files_to_fix_examples/sample.kt") - col = UploadCollector(None, None, None) + col = UploadCollector(None, None, None, None) fixes = col._produce_file_fixes([kt_file]) @@ -31,7 +31,7 @@ def test_fix_kt_files(): def test_fix_go_files(): go_file = Path("tests/data/files_to_fix_examples/sample.go") - col = UploadCollector(None, None, None) + col = UploadCollector(None, None, None, None) fixes = col._produce_file_fixes([go_file]) @@ -57,7 +57,7 @@ def test_fix_bad_encoding_files(mock_open): mock_open.side_effect = UnicodeDecodeError("", bytes(), 0, 0, "") go_file = Path("tests/data/files_to_fix_examples/bad_encoding.go") - col = UploadCollector(None, None, None) + col = UploadCollector(None, None, None, None) fixes = col._produce_file_fixes([go_file]) assert len(fixes) == 1 @@ -70,7 +70,7 @@ def test_fix_bad_encoding_files(mock_open): def test_fix_php_files(): php_file = Path("tests/data/files_to_fix_examples/sample.php") - col = UploadCollector(None, None, None) + col = UploadCollector(None, None, None, None) fixes = col._produce_file_fixes([php_file]) @@ -85,7 +85,7 @@ def test_fix_php_files(): def test_fix_for_cpp_swift_vala(tmp_path): cpp_file = Path("tests/data/files_to_fix_examples/sample.cpp") - col = UploadCollector(None, None, None) + col = UploadCollector(None, None, None, None) fixes = col._produce_file_fixes([cpp_file]) @@ -107,7 +107,7 @@ def test_fix_for_cpp_swift_vala(tmp_path): def test_fix_when_disabled_fixes(tmp_path): cpp_file = Path("tests/data/files_to_fix_examples/sample.cpp") - col = UploadCollector(None, None, None, True) + col = UploadCollector(None, None, None, None, True) fixes = col._produce_file_fixes([cpp_file]) @@ -166,7 +166,7 @@ def test_generate_upload_data(tmp_path): network_finder = NetworkFinder(GitVersioningSystem(), None, None, None) - collector = UploadCollector([], network_finder, file_finder) + collector = UploadCollector([], network_finder, file_finder, None) res = collector.generate_upload_data() diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 081ee2bb..b6beca3f 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -55,6 +55,10 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): job_code="job_code", env_vars=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, name="name", network_filter=None, network_prefix=None, @@ -81,7 +85,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): assert res == LegacyUploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"] + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -152,6 +156,10 @@ def test_do_upload_logic_happy_path(mocker): job_code="job_code", env_vars=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, name="name", network_filter=None, network_prefix=None, @@ -176,7 +184,7 @@ def test_do_upload_logic_happy_path(mocker): assert res == UploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"] + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -243,6 +251,10 @@ def test_do_upload_logic_dry_run(mocker): job_code="job_code", env_vars=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, name="name", network_filter=None, network_prefix=None, @@ -270,7 +282,7 @@ def test_do_upload_logic_dry_run(mocker): assert mock_generate_upload_data.call_count == 1 assert mock_send_upload_data.call_count == 0 mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"] + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} ) assert out_bytes == [ ("info", "dry-run option activated. NOT sending data to Codecov."), @@ -303,30 +315,34 @@ def test_do_upload_logic_verbose(mocker, use_verbose_option): cli_config, versioning_system, ci_adapter, - upload_file_type="coverage", - commit_sha="commit_sha", - report_code="report_code", + branch="branch", build_code="build_code", build_url="build_url", - job_code="job_code", + commit_sha="commit_sha", + dry_run=True, + enterprise_url=None, env_vars=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, + files_search_root_folder=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, + git_service="git_service", + job_code="job_code", name="name", network_filter=None, network_prefix=None, network_root_folder=None, - files_search_root_folder=None, - files_search_exclude_folders=None, - files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], - token="token", - branch="branch", + pull_request_number="pr", + report_code="report_code", slug="slug", + token="token", + upload_file_type="coverage", use_legacy_uploader=True, - pull_request_number="pr", - dry_run=True, - git_service="git_service", - enterprise_url=None, ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ @@ -388,6 +404,10 @@ def side_effect(*args, **kwargs): job_code="job_code", env_vars=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, name="name", network_filter=None, network_prefix=None, @@ -418,7 +438,7 @@ def side_effect(*args, **kwargs): text="No coverage reports found. Triggering notificaions without uploading.", ) mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"] + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -476,6 +496,10 @@ def side_effect(*args, **kwargs): job_code="job_code", env_vars=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, name="name", network_filter=None, network_prefix=None, @@ -497,7 +521,7 @@ def side_effect(*args, **kwargs): == "No coverage reports found. Please make sure you're generating reports successfully." ) mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"] + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -548,6 +572,10 @@ def test_do_upload_logic_happy_path_test_results(mocker): job_code="job_code", env_vars=None, flags=None, + gcov_args=None, + gcov_executable=None, + gcov_ignore=None, + gcov_include=None, name="name", network_filter="some_dir", network_prefix="hello/", From 5e6fee000be1916f246e08f3f8f3eeb9c2ecae23 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:48:09 -0500 Subject: [PATCH 108/128] feat: add xcode functionality (#537) * first pass * fix: update tests * feat: allow for swift coverage * fix: rebase and tests --- codecov_cli/commands/upload.py | 6 +++ codecov_cli/commands/upload_process.py | 2 + codecov_cli/plugins/__init__.py | 4 +- codecov_cli/plugins/xcode.py | 9 ++-- codecov_cli/services/upload/__init__.py | 2 + tests/commands/test_invoke_upload_process.py | 1 + tests/services/upload/test_upload_service.py | 43 ++++++++++++-------- 7 files changed, 44 insertions(+), 23 deletions(-) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index e191fae0..2b78f687 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -194,6 +194,10 @@ def _turn_env_vars_into_dict(ctx, params, value): "--gcov-executable", help="gcov executable to run. Defaults to 'gcov'", ), + click.option( + "--swift-project", + help="Specify the swift project", + ), ] @@ -238,6 +242,7 @@ def do_upload( pull_request_number: typing.Optional[str], report_type: str, slug: typing.Optional[str], + swift_project: typing.Optional[str], token: typing.Optional[str], use_legacy_uploader: bool, ): @@ -286,6 +291,7 @@ def do_upload( pull_request_number=pull_request_number, report_code=report_code, slug=slug, + swift_project=swift_project, token=token, upload_file_type=report_type, use_legacy_uploader=use_legacy_uploader, diff --git a/codecov_cli/commands/upload_process.py b/codecov_cli/commands/upload_process.py index b30cdb27..1ee77eca 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -55,6 +55,7 @@ def upload_process( report_code: str, report_type: str, slug: typing.Optional[str], + swift_project: typing.Optional[str], token: typing.Optional[str], use_legacy_uploader: bool, ): @@ -118,6 +119,7 @@ def upload_process( report_code=report_code, report_type=report_type, slug=slug, + swift_project=swift_project, token=token, use_legacy_uploader=use_legacy_uploader, ) diff --git a/codecov_cli/plugins/__init__.py b/codecov_cli/plugins/__init__.py index df17c8ab..db7a8a4d 100644 --- a/codecov_cli/plugins/__init__.py +++ b/codecov_cli/plugins/__init__.py @@ -80,7 +80,9 @@ def _get_plugin(cli_config, plugin_name, plugin_config): config = cli_config.get("plugins", {}).get("pycoverage", {}) return Pycoverage(config) if plugin_name == "xcode": - return XcodePlugin() + return XcodePlugin( + plugin_config.get("swift_project", None), + ) if plugin_name == "compress-pycoverage": config = cli_config.get("plugins", {}).get("compress-pycoverage", {}) return CompressPycoverageContexts(config) diff --git a/codecov_cli/plugins/xcode.py b/codecov_cli/plugins/xcode.py index a0dae751..d8e4d0db 100644 --- a/codecov_cli/plugins/xcode.py +++ b/codecov_cli/plugins/xcode.py @@ -16,12 +16,13 @@ class XcodePlugin(object): def __init__( self, + app_name: typing.Optional[str] = None, derived_data_folder: typing.Optional[pathlib.Path] = None, - app_name: typing.Optional[pathlib.Path] = None, ): - self.derived_data_folder = pathlib.Path( - derived_data_folder or "~/Library/Developer/Xcode/DerivedData" - ).expanduser() + self.derived_data_folder = ( + derived_data_folder + or pathlib.Path("~/Library/Developer/Xcode/DerivedData").expanduser() + ) # this is to speed up processing and to build reports for the project being tested, # if empty the plugin will build reports for every xcode project it finds diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index c9f2b779..ae1a22a2 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -55,6 +55,7 @@ def do_upload_logic( pull_request_number: typing.Optional[str], report_code: str, slug: typing.Optional[str], + swift_project: typing.Optional[str], token: str, upload_file_type: str = "coverage", use_legacy_uploader: bool = False, @@ -66,6 +67,7 @@ def do_upload_logic( "gcov_ignore": gcov_ignore, "gcov_include": gcov_include, "project_root": files_search_root_folder, + "swift_project": swift_project, } if upload_file_type == "coverage": preparation_plugins = select_preparation_plugins( diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 8f6fcbdc..59f70851 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -130,6 +130,7 @@ def test_upload_process_options(mocker): " --gcov-ignore TEXT Paths to ignore during gcov gathering", " --gcov-include TEXT Paths to include during gcov gathering", " --gcov-executable TEXT gcov executable to run. Defaults to 'gcov'", + " --swift-project TEXT Specify the swift project", " --parent-sha TEXT SHA (with 40 chars) of what should be the", " parent of this commit", " -h, --help Show this message and exit.", diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index b6beca3f..7c33aba2 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -71,6 +71,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): branch="branch", use_legacy_uploader=True, slug="slug", + swift_project="App", pull_request_number="pr", git_service="git_service", enterprise_url=None, @@ -85,7 +86,7 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): assert res == LegacyUploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -171,6 +172,7 @@ def test_do_upload_logic_happy_path(mocker): token="token", branch="branch", slug="slug", + swift_project="App", pull_request_number="pr", git_service="git_service", enterprise_url=None, @@ -184,7 +186,7 @@ def test_do_upload_logic_happy_path(mocker): assert res == UploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -266,6 +268,7 @@ def test_do_upload_logic_dry_run(mocker): token="token", branch="branch", slug="slug", + swift_project="App", pull_request_number="pr", dry_run=True, git_service="git_service", @@ -282,7 +285,7 @@ def test_do_upload_logic_dry_run(mocker): assert mock_generate_upload_data.call_count == 1 assert mock_send_upload_data.call_count == 0 mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} ) assert out_bytes == [ ("info", "dry-run option activated. NOT sending data to Codecov."), @@ -340,6 +343,7 @@ def test_do_upload_logic_verbose(mocker, use_verbose_option): pull_request_number="pr", report_code="report_code", slug="slug", + swift_project="App", token="token", upload_file_type="coverage", use_legacy_uploader=True, @@ -419,6 +423,7 @@ def side_effect(*args, **kwargs): token="token", branch="branch", slug="slug", + swift_project="App", pull_request_number="pr", git_service="git_service", enterprise_url=None, @@ -438,7 +443,7 @@ def side_effect(*args, **kwargs): text="No coverage reports found. Triggering notificaions without uploading.", ) mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -511,6 +516,7 @@ def side_effect(*args, **kwargs): token="token", branch="branch", slug="slug", + swift_project="App", pull_request_number="pr", git_service="git_service", enterprise_url=None, @@ -521,7 +527,7 @@ def side_effect(*args, **kwargs): == "No coverage reports found. Please make sure you're generating reports successfully." ) mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None} + cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -564,33 +570,34 @@ def test_do_upload_logic_happy_path_test_results(mocker): cli_config, versioning_system, ci_adapter, - upload_file_type="test_results", - commit_sha="commit_sha", - report_code="report_code", + args={"args": "fake_args"}, + branch="branch", build_code="build_code", build_url="build_url", - job_code="job_code", + commit_sha="commit_sha", + enterprise_url=None, env_vars=None, + files_search_exclude_folders=None, + files_search_explicitly_listed_files=None, + files_search_root_folder=None, flags=None, gcov_args=None, gcov_executable=None, gcov_ignore=None, gcov_include=None, + git_service="git_service", + job_code="job_code", name="name", network_filter="some_dir", network_prefix="hello/", network_root_folder="root/", - files_search_root_folder=None, - files_search_exclude_folders=None, - files_search_explicitly_listed_files=None, plugin_names=["first_plugin", "another", "forth"], - token="token", - branch="branch", - slug="slug", pull_request_number="pr", - git_service="git_service", - enterprise_url=None, - args={"args": "fake_args"}, + report_code="report_code", + slug="slug", + swift_project="App", + token="token", + upload_file_type="test_results", ) out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) assert out_bytes == [ From 08745b94524bb789ad8df39980f6b8ebc45d4a51 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 17 Oct 2024 12:02:20 -0400 Subject: [PATCH 109/128] Prepare release 0.8.0 (#538) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9e604bc..b1c59f32 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.6", + version="0.8.0", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From d5ef60060b72f29965868098644fa3bd275aca87 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:26:48 -0500 Subject: [PATCH 110/128] fix: move to ingest.codecov.io (#542) * fix: move to ingest.codecov.io * fix: update tests * fix: lint * fix: really lint --- Makefile | 2 +- codecov_cli/helpers/config.py | 1 + codecov_cli/services/commit/__init__.py | 4 +- codecov_cli/services/report/__init__.py | 4 +- codecov_cli/services/upload/upload_sender.py | 4 +- tests/commands/test_process_test_results.py | 1 + tests/helpers/test_upload_sender.py | 10 ++-- tests/plugins/test_instantiation.py | 12 ++-- tests/services/commit/test_commit_service.py | 2 +- tests/services/upload/test_upload_service.py | 60 ++++++++++++++++++-- 10 files changed, 78 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index f70eab15..aed79ccb 100644 --- a/Makefile +++ b/Makefile @@ -16,4 +16,4 @@ else @echo "Tagging new release ${version}" git tag -a ${version} -m "Autogenerated release tag for codecov-cli" git push origin ${version} -endif \ No newline at end of file +endif diff --git a/codecov_cli/helpers/config.py b/codecov_cli/helpers/config.py index 870ac525..2c87ded2 100644 --- a/codecov_cli/helpers/config.py +++ b/codecov_cli/helpers/config.py @@ -9,6 +9,7 @@ logger = logging.getLogger("codecovcli") CODECOV_API_URL = "https://api.codecov.io" +CODECOV_INGEST_URL = "https://ingest.codecov.io" LEGACY_CODECOV_API_URL = "https://codecov.io" # Relative to the project root diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index 85c4d256..c142e7bf 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -2,7 +2,7 @@ import os import typing -from codecov_cli.helpers.config import CODECOV_API_URL +from codecov_cli.helpers.config import CODECOV_INGEST_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug from codecov_cli.helpers.request import ( get_token_header_or_fail, @@ -71,7 +71,7 @@ def send_commit_data( "pullid": pr, } - upload_url = enterprise_url or CODECOV_API_URL + upload_url = enterprise_url or CODECOV_INGEST_URL url = f"{upload_url}/upload/{service}/{slug}/commits" return send_post_request( url=url, diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index a3734ad1..0b29c9d6 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -5,7 +5,7 @@ import requests from codecov_cli.helpers import request -from codecov_cli.helpers.config import CODECOV_API_URL +from codecov_cli.helpers.config import CODECOV_API_URL, CODECOV_INGEST_URL from codecov_cli.helpers.encoder import decode_slug, encode_slug from codecov_cli.helpers.request import ( get_token_header, @@ -60,7 +60,7 @@ def send_create_report_request( "code": code, } headers = get_token_header(token) - upload_url = enterprise_url or CODECOV_API_URL + upload_url = enterprise_url or CODECOV_INGEST_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports" return send_post_request(url=url, headers=headers, data=data) diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index 22f8924a..bfd5a07f 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -6,7 +6,7 @@ from typing import Any, Dict from codecov_cli import __version__ as codecov_cli_version -from codecov_cli.helpers.config import CODECOV_API_URL +from codecov_cli.helpers.config import CODECOV_INGEST_URL from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.request import ( get_token_header, @@ -56,7 +56,7 @@ def send_upload_data( } headers = get_token_header(token) encoded_slug = encode_slug(slug) - upload_url = enterprise_url or CODECOV_API_URL + upload_url = enterprise_url or CODECOV_INGEST_URL url, data = self.get_url_and_possibly_update_data( data, upload_file_type, diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index e583159d..9ef89adc 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -43,6 +43,7 @@ def test_process_test_results( # Ensure that there's an output assert result.output + def test_process_test_results_create_github_message( mocker, tmpdir, diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 68dd840b..642e2a35 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -69,7 +69,7 @@ def mocked_legacy_upload_endpoint(mocked_responses): encoded_slug = encode_slug(named_upload_data["slug"]) resp = responses.Response( responses.POST, - f"https://api.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads", + f"https://ingest.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads", status=200, json={ "raw_upload_location": "https://puturl.com", @@ -84,7 +84,7 @@ def mocked_legacy_upload_endpoint(mocked_responses): def mocked_test_results_endpoint(mocked_responses): resp = responses.Response( responses.POST, - f"https://api.codecov.io/upload/test_results/v1", + f"https://ingest.codecov.io/upload/test_results/v1", status=200, json={ "raw_upload_location": "https://puturl.com", @@ -187,7 +187,7 @@ def test_upload_sender_post_called_with_right_parameters( assert response.get("url") == "https://app.codecov.io/commit-url" assert ( post_req_made.url - == f"https://api.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads" + == f"https://ingest.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads" ) assert ( post_req_made.headers.items() >= headers.items() @@ -217,7 +217,7 @@ def test_upload_sender_post_called_with_right_parameters_test_results( post_req_made = mocked_responses.calls[0].request response = json.loads(mocked_responses.calls[0].response.text) assert response.get("raw_upload_location") == "https://puturl.com" - assert post_req_made.url == "https://api.codecov.io/upload/test_results/v1" + assert post_req_made.url == "https://ingest.codecov.io/upload/test_results/v1" assert ( post_req_made.headers.items() >= headers.items() ) # test dict is a subset of the other @@ -254,7 +254,7 @@ def test_upload_sender_post_called_with_right_parameters_tokenless( assert response.get("url") == "https://app.codecov.io/commit-url" assert ( post_req_made.url - == f"https://api.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads" + == f"https://ingest.codecov.io/upload/github/{encoded_slug}/commits/{random_sha}/reports/{named_upload_data['report_code']}/uploads" ) assert ( post_req_made.headers.items() >= headers.items() diff --git a/tests/plugins/test_instantiation.py b/tests/plugins/test_instantiation.py index 97e5b68a..fdf3a842 100644 --- a/tests/plugins/test_instantiation.py +++ b/tests/plugins/test_instantiation.py @@ -109,9 +109,13 @@ def test_get_plugin_gcov(): res = _get_plugin({}, "gcov", {}) assert isinstance(res, GcovPlugin) - res = _get_plugin({}, "gcov", { - 'gcov_executable': 'lcov', - }) + res = _get_plugin( + {}, + "gcov", + { + "gcov_executable": "lcov", + }, + ) assert isinstance(res, GcovPlugin) @@ -186,7 +190,7 @@ def __init__(self, banana=None): } }, ["gcov", "something", "otherthing", "second", "lalalala"], - {} + {}, ) assert len(res) == 5 assert isinstance(res[0], GcovPlugin) diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 3f4ae1fd..31ac3c02 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -168,7 +168,7 @@ def test_commit_sender_with_forked_repo(mocker): None, ) mocked_response.assert_called_with( - url="https://api.codecov.io/upload/github/codecov::::codecov-cli/commits", + url="https://ingest.codecov.io/upload/github/codecov::::codecov-cli/commits", data={ "branch": "user_forked_repo/codecov-cli:branch", "cli_args": None, diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 7c33aba2..9a38a5f9 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -86,7 +86,17 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): assert res == LegacyUploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} + cli_config, + ["first_plugin", "another", "forth"], + { + "folders_to_ignore": None, + "gcov_args": None, + "gcov_executable": None, + "gcov_ignore": None, + "gcov_include": None, + "project_root": None, + "swift_project": "App", + }, ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -186,7 +196,17 @@ def test_do_upload_logic_happy_path(mocker): assert res == UploadSender.send_upload_data.return_value mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} + cli_config, + ["first_plugin", "another", "forth"], + { + "folders_to_ignore": None, + "gcov_args": None, + "gcov_executable": None, + "gcov_ignore": None, + "gcov_include": None, + "project_root": None, + "swift_project": "App", + }, ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -285,7 +305,17 @@ def test_do_upload_logic_dry_run(mocker): assert mock_generate_upload_data.call_count == 1 assert mock_send_upload_data.call_count == 0 mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} + cli_config, + ["first_plugin", "another", "forth"], + { + "folders_to_ignore": None, + "gcov_args": None, + "gcov_executable": None, + "gcov_ignore": None, + "gcov_include": None, + "project_root": None, + "swift_project": "App", + }, ) assert out_bytes == [ ("info", "dry-run option activated. NOT sending data to Codecov."), @@ -443,7 +473,17 @@ def side_effect(*args, **kwargs): text="No coverage reports found. Triggering notificaions without uploading.", ) mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} + cli_config, + ["first_plugin", "another", "forth"], + { + "folders_to_ignore": None, + "gcov_args": None, + "gcov_executable": None, + "gcov_ignore": None, + "gcov_include": None, + "project_root": None, + "swift_project": "App", + }, ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( @@ -527,7 +567,17 @@ def side_effect(*args, **kwargs): == "No coverage reports found. Please make sure you're generating reports successfully." ) mock_select_preparation_plugins.assert_called_with( - cli_config, ["first_plugin", "another", "forth"], {'folders_to_ignore': None, 'gcov_args': None, 'gcov_executable': None, 'gcov_ignore': None, 'gcov_include': None, 'project_root': None, 'swift_project': 'App'} + cli_config, + ["first_plugin", "another", "forth"], + { + "folders_to_ignore": None, + "gcov_args": None, + "gcov_executable": None, + "gcov_ignore": None, + "gcov_include": None, + "project_root": None, + "swift_project": "App", + }, ) mock_select_file_finder.assert_called_with(None, None, None, False, "coverage") mock_select_network_finder.assert_called_with( From db6ab021b52dc6564910749e77aa622a79cbd964 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Mon, 28 Oct 2024 09:58:24 +0100 Subject: [PATCH 111/128] Document `empty-upload --force` flag (#543) --- README.md | 17 +++++++++-------- codecov_cli/helpers/request.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 457a2307..81c2b179 100644 --- a/README.md +++ b/README.md @@ -240,14 +240,15 @@ are ignored by codecov (including README and configuration files) `Usage: codecovcli empty-upload [OPTIONS]` -| Options | Description | usage -| :---: | :---: | :---: | - | -C, --sha, --commit-sha TEXT |Commit SHA (with 40 chars) | Required - | -t, --token TEXT |Codecov upload token |Required -| -r, --slug TEXT | owner/repo slug used instead of the private repo token in Self-hosted | Optional -| -Z, --fail-on-error | Exit with non-zero code in case of error|Optional -| --git-service| Options: github, gitlab, bitbucket, github_enterprise, gitlab_enterprise, bitbucket_server | Optional -| -h, --help | Show this message and exit.|Optional +| Options | Description | usage | +| :--------------------------: | :----------------------------------------------------------------------------------------: | :------: | +| -C, --sha, --commit-sha TEXT | Commit SHA (with 40 chars) | Required | +| -t, --token TEXT | Codecov upload token | Required | +| -r, --slug TEXT | owner/repo slug used instead of the private repo token in Self-hosted | Optional | +| --force | Always emit passing checks regardless of changed files | Optional | +| -Z, --fail-on-error | Exit with non-zero code in case of error | Optional | +| --git-service | Options: github, gitlab, bitbucket, github_enterprise, gitlab_enterprise, bitbucket_server | Optional | +| -h, --help | Show this message and exit. | Optional | # How to Use Local Upload diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index 6c153bd5..f9cf8ab1 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -125,7 +125,7 @@ def send_put_request( return request_result(put(url=url, data=data, headers=headers)) -def request_result(resp): +def request_result(resp: requests.Response) -> RequestResult: if resp.status_code >= 400: return RequestResult( status_code=resp.status_code, From 880d3fde12a23f3010d5923ba5c9b91dd9c079cd Mon Sep 17 00:00:00 2001 From: Nora Shapiro Date: Thu, 7 Nov 2024 12:00:05 -0800 Subject: [PATCH 112/128] Remove token enforcement for true tokenless endpoints (#533) * setup and cleanup * remove token enforcement for true tokenless endpoints * backwards compatability * remove pytest-mock * typing fixes * fix: bump version * fix: logs * all upload endpoints can do tokenless * fix failing test * remove test log * bump version * revert changes to version * linter changes --------- Co-authored-by: Tom Hu --- codecov_cli/helpers/request.py | 10 ++++- codecov_cli/services/commit/__init__.py | 8 ++-- codecov_cli/services/commit/base_picking.py | 4 +- codecov_cli/services/empty_upload/__init__.py | 4 +- codecov_cli/services/report/__init__.py | 14 +++---- codecov_cli/services/upload/__init__.py | 2 +- codecov_cli/services/upload/upload_sender.py | 2 +- tests/services/commit/test_base_picking.py | 24 ++++++++++++ tests/services/commit/test_commit_service.py | 30 ++++++++++++++ .../empty_upload/test_empty_upload.py | 30 ++++++++++++++ tests/services/report/test_report_results.py | 39 ++++++++++++++++--- tests/services/report/test_report_service.py | 24 +++++++++++- .../test_upload_completion.py | 31 +++++++++++++++ 13 files changed, 195 insertions(+), 27 deletions(-) diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index f9cf8ab1..e5c04f8f 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -102,7 +102,10 @@ def send_get_request( return request_result(get(url=url, headers=headers, params=params)) -def get_token_header_or_fail(token: str) -> dict: +def get_token_header_or_fail(token: Optional[str]) -> dict: + """ + Rejects requests with no Authorization token. Prevents tokenless uploads. + """ if token is None: raise click.ClickException( "Codecov token not found. Please provide Codecov token with -t flag." @@ -110,7 +113,10 @@ def get_token_header_or_fail(token: str) -> dict: return {"Authorization": f"token {token}"} -def get_token_header(token: str) -> Optional[dict]: +def get_token_header(token: Optional[str]) -> Optional[dict]: + """ + Allows requests with no Authorization token. + """ if token is None: return None return {"Authorization": f"token {token}"} diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index c142e7bf..54518de7 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -3,9 +3,9 @@ import typing from codecov_cli.helpers.config import CODECOV_INGEST_URL -from codecov_cli.helpers.encoder import decode_slug, encode_slug +from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.request import ( - get_token_header_or_fail, + get_token_header, log_warnings_and_errors_if_any, send_post_request, ) @@ -19,7 +19,7 @@ def create_commit_logic( pr: typing.Optional[str], branch: typing.Optional[str], slug: typing.Optional[str], - token: str, + token: typing.Optional[str], service: typing.Optional[str], enterprise_url: typing.Optional[str] = None, fail_on_error: bool = False, @@ -61,7 +61,7 @@ def send_commit_data( branch = tokenless # type: ignore logger.info("The PR is happening in a forked repo. Using tokenless upload.") else: - headers = get_token_header_or_fail(token) + headers = get_token_header(token) data = { "branch": branch, diff --git a/codecov_cli/services/commit/base_picking.py b/codecov_cli/services/commit/base_picking.py index 20767132..7332a462 100644 --- a/codecov_cli/services/commit/base_picking.py +++ b/codecov_cli/services/commit/base_picking.py @@ -2,7 +2,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.request import ( - get_token_header_or_fail, + get_token_header, log_warnings_and_errors_if_any, send_put_request, ) @@ -15,7 +15,7 @@ def base_picking_logic(base_sha, pr, slug, token, service, enterprise_url, args) "cli_args": args, "user_provided_base_sha": base_sha, } - headers = get_token_header_or_fail(token) + headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/api/v1/{service}/{slug}/pulls/{pr}" sending_result = send_put_request(url=url, data=data, headers=headers) diff --git a/codecov_cli/services/empty_upload/__init__.py b/codecov_cli/services/empty_upload/__init__.py index 7c8b0682..587bb756 100644 --- a/codecov_cli/services/empty_upload/__init__.py +++ b/codecov_cli/services/empty_upload/__init__.py @@ -4,7 +4,7 @@ from codecov_cli.helpers.config import CODECOV_API_URL from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.request import ( - get_token_header_or_fail, + get_token_header, log_warnings_and_errors_if_any, send_post_request, ) @@ -23,7 +23,7 @@ def empty_upload_logic( args, ): encoded_slug = encode_slug(slug) - headers = get_token_header_or_fail(token) + headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/empty-upload" sending_result = send_post_request( diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index 0b29c9d6..b19a8c19 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -1,15 +1,13 @@ import json import logging import time - -import requests +import typing from codecov_cli.helpers import request from codecov_cli.helpers.config import CODECOV_API_URL, CODECOV_INGEST_URL -from codecov_cli.helpers.encoder import decode_slug, encode_slug +from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.request import ( get_token_header, - get_token_header_or_fail, log_warnings_and_errors_if_any, request_result, send_post_request, @@ -24,7 +22,7 @@ def create_report_logic( code: str, slug: str, service: str, - token: str, + token: typing.Optional[str], enterprise_url: str, pull_request_number: int, fail_on_error: bool = False, @@ -70,7 +68,7 @@ def create_report_results_logic( code: str, slug: str, service: str, - token: str, + token: typing.Optional[str], enterprise_url: str, fail_on_error: bool = False, args: dict = None, @@ -103,7 +101,7 @@ def send_reports_result_request( data = { "cli_args": args, } - headers = get_token_header_or_fail(token) + headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/results" return send_post_request(url=url, data=data, headers=headers) @@ -118,7 +116,7 @@ def send_reports_result_get_request( enterprise_url, fail_on_error=False, ): - headers = get_token_header_or_fail(token) + headers = get_token_header(token) upload_url = enterprise_url or CODECOV_API_URL url = f"{upload_url}/upload/{service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/results" number_tries = 0 diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index ae1a22a2..7b3c884f 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -56,7 +56,7 @@ def do_upload_logic( report_code: str, slug: typing.Optional[str], swift_project: typing.Optional[str], - token: str, + token: typing.Optional[str], upload_file_type: str = "coverage", use_legacy_uploader: bool = False, ): diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index bfd5a07f..84dc8189 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -27,7 +27,7 @@ def send_upload_data( self, upload_data: UploadCollectionResult, commit_sha: str, - token: str, + token: typing.Optional[str], env_vars: typing.Dict[str, str], report_code: str, upload_file_type: str = "coverage", diff --git a/tests/services/commit/test_base_picking.py b/tests/services/commit/test_base_picking.py index 9b560a51..2c295fe2 100644 --- a/tests/services/commit/test_base_picking.py +++ b/tests/services/commit/test_base_picking.py @@ -139,3 +139,27 @@ def test_base_picking_command_error(mocker): "error", "Base picking failed: Unauthorized", ) in parse_outstreams_into_log_lines(result.output) + + +def test_base_picking_no_token(mocker): + mocked_response = mocker.patch( + "codecov_cli.services.commit.base_picking.send_put_request", + return_value=RequestResult(status_code=200, error=None, warnings=[], text=""), + ) + runner = CliRunner() + result = runner.invoke( + pr_base_picking, + [ + "--pr", + "11", + "--base-sha", + "9a6902ee94c18e8e27561ce316b16d75a02c7bc1", + "--service", + "github", + "--slug", + "owner/repo", + ], + obj=mocker.MagicMock(), # context object + ) + assert result.exit_code == 0 + mocked_response.assert_called_once() diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 31ac3c02..2fd5157d 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -178,3 +178,33 @@ def test_commit_sender_with_forked_repo(mocker): }, headers=None, ) + + +def test_commit_without_token(mocker): + mocked_response = mocker.patch( + "codecov_cli.services.commit.send_post_request", + return_value=mocker.MagicMock(status_code=200, text="success"), + ) + + send_commit_data( + "commit_sha", + "parent_sha", + "1", + "branch", + "codecov::::codecov-cli", + None, + "github", + None, + None, + ) + mocked_response.assert_called_with( + url="https://ingest.codecov.io/upload/github/codecov::::codecov-cli/commits", + data={ + "branch": "branch", + "cli_args": None, + "commitid": "commit_sha", + "parent_commit_id": "parent_sha", + "pullid": "1", + }, + headers=None, + ) diff --git a/tests/services/empty_upload/test_empty_upload.py b/tests/services/empty_upload/test_empty_upload.py index b49d055e..1e354d50 100644 --- a/tests/services/empty_upload/test_empty_upload.py +++ b/tests/services/empty_upload/test_empty_upload.py @@ -1,6 +1,8 @@ import json import uuid +import click +import pytest from click.testing import CliRunner from codecov_cli.services.empty_upload import empty_upload_logic @@ -147,3 +149,31 @@ def test_empty_upload_force(mocker): assert res.error is None assert res.warnings == [] mocked_response.assert_called_once() + + +def test_empty_upload_no_token(mocker): + res = { + "result": "All changed files are ignored. Triggering passing notifications.", + "non_ignored_files": [], + } + mocked_response = mocker.patch( + "codecov_cli.helpers.request.requests.post", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text=json.dumps(res) + ), + ) + runner = CliRunner() + with runner.isolation() as outstreams: + res = empty_upload_logic( + "commit_sha", "owner/repo", None, "service", None, False, False, None + ) + + out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) + assert out_bytes == [ + ("info", "Process Empty Upload complete"), + ("info", "All changed files are ignored. Triggering passing notifications."), + ("info", "Non ignored files []"), + ] + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_once() diff --git a/tests/services/report/test_report_results.py b/tests/services/report/test_report_results.py index 27808d23..268f56e3 100644 --- a/tests/services/report/test_report_results.py +++ b/tests/services/report/test_report_results.py @@ -108,6 +108,19 @@ def test_report_results_request_200(mocker): mocked_response.assert_called_once() +def test_report_results_request_no_token(mocker): + mocked_response = mocker.patch( + "codecov_cli.helpers.request.requests.post", + return_value=mocker.MagicMock(status_code=200), + ) + res = send_reports_result_request( + "commit_sha", "report_code", "encoded_slug", "service", None, None, None + ) + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_once() + + def test_report_results_403(mocker): mocked_response = mocker.patch( "codecov_cli.helpers.request.requests.post", @@ -127,7 +140,7 @@ def test_report_results_403(mocker): def test_get_report_results_200_completed(mocker, capsys): mocked_response = mocker.patch( - "codecov_cli.services.report.requests.get", + "codecov_cli.helpers.request.requests.get", return_value=mocker.MagicMock( status_code=200, text='{"state": "completed", "result": {"state": "failure","message": "33.33% of diff hit (target 77.77%)"}}', @@ -147,11 +160,27 @@ def test_get_report_results_200_completed(mocker, capsys): ) in output +def test_get_report_results_no_token(mocker, capsys): + mocked_response = mocker.patch( + "codecov_cli.helpers.request.requests.get", + return_value=mocker.MagicMock( + status_code=200, + text='{"state": "completed", "result": {"state": "failure","message": "33.33% of diff hit (target 77.77%)"}}', + ), + ) + res = send_reports_result_get_request( + "commit_sha", "report_code", "encoded_slug", "service", None, None + ) + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_once() + + @patch("codecov_cli.services.report.MAX_NUMBER_TRIES", 1) def test_get_report_results_200_pending(mocker, capsys): mocker.patch("codecov_cli.services.report.time.sleep") mocked_response = mocker.patch( - "codecov_cli.services.report.requests.get", + "codecov_cli.helpers.request.requests.get", return_value=mocker.MagicMock( status_code=200, text='{"state": "pending", "result": {}}' ), @@ -169,7 +198,7 @@ def test_get_report_results_200_pending(mocker, capsys): def test_get_report_results_200_error(mocker, capsys): mocked_response = mocker.patch( - "codecov_cli.services.report.requests.get", + "codecov_cli.helpers.request.requests.get", return_value=mocker.MagicMock( status_code=200, text='{"state": "error", "result": {}}' ), @@ -190,7 +219,7 @@ def test_get_report_results_200_error(mocker, capsys): def test_get_report_results_200_undefined_state(mocker, capsys): mocked_response = mocker.patch( - "codecov_cli.services.report.requests.get", + "codecov_cli.helpers.request.requests.get", return_value=mocker.MagicMock( status_code=200, text='{"state": "undefined_state", "result": {}}' ), @@ -208,7 +237,7 @@ def test_get_report_results_200_undefined_state(mocker, capsys): def test_get_report_results_401(mocker, capsys): mocked_response = mocker.patch( - "codecov_cli.services.report.requests.get", + "codecov_cli.helpers.request.requests.get", return_value=mocker.MagicMock( status_code=401, text='{"detail": "Invalid token."}' ), diff --git a/tests/services/report/test_report_service.py b/tests/services/report/test_report_service.py index ac4c222b..153e803a 100644 --- a/tests/services/report/test_report_service.py +++ b/tests/services/report/test_report_service.py @@ -9,7 +9,7 @@ def test_send_create_report_request_200(mocker): mocked_response = mocker.patch( - "codecov_cli.services.report.requests.post", + "codecov_cli.helpers.request.requests.post", return_value=mocker.MagicMock(status_code=200), ) res = send_create_report_request( @@ -27,9 +27,29 @@ def test_send_create_report_request_200(mocker): mocked_response.assert_called_once() +def test_send_create_report_request_no_token(mocker): + mocked_response = mocker.patch( + "codecov_cli.helpers.request.requests.post", + return_value=mocker.MagicMock(status_code=200), + ) + res = send_create_report_request( + "commit_sha", + "code", + "github", + None, + "owner::::repo", + "enterprise_url", + 1, + None, + ) + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_once() + + def test_send_create_report_request_403(mocker): mocked_response = mocker.patch( - "codecov_cli.services.report.requests.post", + "codecov_cli.helpers.request.requests.post", return_value=mocker.MagicMock(status_code=403, text="Permission denied"), ) res = send_create_report_request( diff --git a/tests/services/upload_completion/test_upload_completion.py b/tests/services/upload_completion/test_upload_completion.py index 6ab5ea3b..6a6d5494 100644 --- a/tests/services/upload_completion/test_upload_completion.py +++ b/tests/services/upload_completion/test_upload_completion.py @@ -1,6 +1,8 @@ import json import uuid +import click +import pytest from click.testing import CliRunner from codecov_cli.services.upload_completion import upload_completion_logic @@ -93,6 +95,35 @@ def test_upload_completion_200(mocker): mocked_response.assert_called_once() +def test_upload_completion_no_token(mocker): + res = { + "uploads_total": 2, + "uploads_success": 2, + "uploads_processing": 0, + "uploads_error": 0, + } + mocked_response = mocker.patch( + "codecov_cli.helpers.request.requests.post", + return_value=RequestResult( + status_code=200, error=None, warnings=[], text=json.dumps(res) + ), + ) + runner = CliRunner() + with runner.isolation() as outstreams: + res = upload_completion_logic("commit_sha", "owner/repo", None, "service", None) + out_bytes = parse_outstreams_into_log_lines(outstreams[0].getvalue()) + assert out_bytes == [ + ("info", "Process Upload Completion complete"), + ( + "info", + "{'uploads_total': 2, 'uploads_success': 2, 'uploads_processing': 0, 'uploads_error': 0}", + ), + ] + assert res.error is None + assert res.warnings == [] + mocked_response.assert_called_once() + + def test_upload_completion_403(mocker): mocked_response = mocker.patch( "codecov_cli.helpers.request.requests.post", From 2935526caf5db25d64b02870f9cc45e33a7daa3d Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 13 Nov 2024 14:35:06 -0500 Subject: [PATCH 113/128] Release v0.9.0 (#553) * Prepare release v0.9.0 * Update setup.py --------- Co-authored-by: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b1c59f32..d905447e 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.8.0", + version="0.9.0", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 01046d1c6d4ab4a6febe9ee03a377496a73441a2 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:15:24 -0500 Subject: [PATCH 114/128] fix: pass args to send_reports_result_request (#545) * fix: pass args to send_reports_result_request * chore: lint * test: fix tests * fix: typing and tests * fix: typing * chore: make lint * fix: simple import typing fix --- codecov_cli/services/report/__init__.py | 7 +++++-- tests/services/report/test_report_results.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/codecov_cli/services/report/__init__.py b/codecov_cli/services/report/__init__.py index b19a8c19..da2b5127 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -3,6 +3,8 @@ import time import typing +import requests + from codecov_cli.helpers import request from codecov_cli.helpers.config import CODECOV_API_URL, CODECOV_INGEST_URL from codecov_cli.helpers.encoder import encode_slug @@ -26,7 +28,7 @@ def create_report_logic( enterprise_url: str, pull_request_number: int, fail_on_error: bool = False, - args: dict = None, + args: typing.Union[dict, None] = None, ): encoded_slug = encode_slug(slug) sending_result = send_create_report_request( @@ -71,7 +73,7 @@ def create_report_results_logic( token: typing.Optional[str], enterprise_url: str, fail_on_error: bool = False, - args: dict = None, + args: typing.Union[dict, None] = None, ): encoded_slug = encode_slug(slug) sending_result = send_reports_result_request( @@ -81,6 +83,7 @@ def create_report_results_logic( service=service, token=token, enterprise_url=enterprise_url, + args=args, ) log_warnings_and_errors_if_any( diff --git a/tests/services/report/test_report_results.py b/tests/services/report/test_report_results.py index 268f56e3..f713ce6b 100644 --- a/tests/services/report/test_report_results.py +++ b/tests/services/report/test_report_results.py @@ -43,6 +43,7 @@ def test_report_results_command_with_warnings(mocker): assert res == mock_send_reports_result_request.return_value mock_send_reports_result_request.assert_called_with( + args=None, commit_sha="commit_sha", report_code="code", service="service", @@ -85,6 +86,7 @@ def test_report_results_command_with_error(mocker): ] assert res == mock_send_reports_result_request.return_value mock_send_reports_result_request.assert_called_with( + args=None, commit_sha="commit_sha", report_code="code", service="service", From df923a22438716e76df66ac08a37e1e47896117b Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 13 Nov 2024 15:24:06 -0500 Subject: [PATCH 115/128] Prepare release 9.0.1 (#555) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d905447e..741dd3ef 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.9.0", + version="9.0.1", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 15922dfada970bececa31b0bd348e6feda1f1d71 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:47:21 -0500 Subject: [PATCH 116/128] fix: downgrade pypi action (#556) --- .github/workflows/build_for_pypi.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build_for_pypi.yml b/.github/workflows/build_for_pypi.yml index 05d9793e..9e7ecc9c 100644 --- a/.github/workflows/build_for_pypi.yml +++ b/.github/workflows/build_for_pypi.yml @@ -35,7 +35,4 @@ jobs: python setup.py bdist_wheel --plat-name=win_amd64 - name: Publish package to PyPi if: inputs.publish == true - uses: pypa/gh-action-pypi-publish@release/v1 - - - + uses: pypa/gh-action-pypi-publish@v1.11.0 # Currently an issue with attestations on `release/v1` From be51b525ad488d552174d8f094ac5bda62e15357 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 13 Nov 2024 15:55:31 -0500 Subject: [PATCH 117/128] Prepare release 9.0.2 (#557) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 741dd3ef..d93811c8 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="9.0.1", + version="9.0.2", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 639aef803387d11d8a1e85df816a2a2d5794bb30 Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:08:58 -0500 Subject: [PATCH 118/128] fix: add verbose logging (#558) --- .github/workflows/build_for_pypi.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_for_pypi.yml b/.github/workflows/build_for_pypi.yml index 9e7ecc9c..ffc0ee8f 100644 --- a/.github/workflows/build_for_pypi.yml +++ b/.github/workflows/build_for_pypi.yml @@ -35,4 +35,7 @@ jobs: python setup.py bdist_wheel --plat-name=win_amd64 - name: Publish package to PyPi if: inputs.publish == true - uses: pypa/gh-action-pypi-publish@v1.11.0 # Currently an issue with attestations on `release/v1` + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + attestation: true From a89f1080e194a8c93c1e7e9b904fa13080d287ad Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 13 Nov 2024 16:16:54 -0500 Subject: [PATCH 119/128] Prepare release 9.0.3 (#559) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d93811c8..645a1606 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="9.0.2", + version="9.0.3", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 84e0ee67dca43d82f5d6e2ecf9a5ae6446c9361a Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:08:42 -0500 Subject: [PATCH 120/128] fix: turn off attestations (#560) --- .github/workflows/build_for_pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_for_pypi.yml b/.github/workflows/build_for_pypi.yml index ffc0ee8f..1315f24a 100644 --- a/.github/workflows/build_for_pypi.yml +++ b/.github/workflows/build_for_pypi.yml @@ -37,5 +37,5 @@ jobs: if: inputs.publish == true uses: pypa/gh-action-pypi-publish@release/v1 with: + attestations: false verbose: true - attestation: true From e680a672bb8e56d0f21d8ea36a22eec061c996af Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Wed, 13 Nov 2024 17:15:37 -0500 Subject: [PATCH 121/128] Prepare release 9.0.4 (#561) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 645a1606..92442f6c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="9.0.3", + version="9.0.4", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From c51e790f868af44d32757abfb51a9ed4a31df575 Mon Sep 17 00:00:00 2001 From: Codecov Releaser Date: Thu, 14 Nov 2024 13:57:59 -0500 Subject: [PATCH 122/128] Prepare release 0.9.4 (#562) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 92442f6c..fd96cd1b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="9.0.4", + version="0.9.4", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, From 0ebc5136c225b548c120f93b22bb9967afce6b9b Mon Sep 17 00:00:00 2001 From: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:37:25 -0500 Subject: [PATCH 123/128] chore(deps): bump httpx to 0.27.x (#552) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fd96cd1b..4ffee44c 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ author_email="support@codecov.io", install_requires=[ "click==8.*", - "httpx==0.23.*", + "httpx==0.27.*", "ijson==3.*", "pyyaml==6.*", "responses==0.21.*", From 1f04560c0cbc203670d39abe64032f5322457721 Mon Sep 17 00:00:00 2001 From: Tony Le Date: Mon, 25 Nov 2024 14:51:55 -0500 Subject: [PATCH 124/128] Adds upload-coverage command (#551) * Adds single endpoint coverage command "upload-coverage" --- codecov_cli/commands/upload_coverage.py | 175 ++++++++++++++++++ codecov_cli/main.py | 2 + codecov_cli/services/upload/__init__.py | 4 + codecov_cli/services/upload/upload_sender.py | 16 +- .../services/upload_coverage/__init__.py | 90 +++++++++ tests/commands/test_invoke_upload_coverage.py | 140 ++++++++++++++ tests/commands/test_invoke_upload_process.py | 2 + tests/helpers/test_upload_sender.py | 41 ++++ tests/services/upload/test_upload_service.py | 6 + tests/test_codecov_cli.py | 1 + 10 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 codecov_cli/commands/upload_coverage.py create mode 100644 codecov_cli/services/upload_coverage/__init__.py create mode 100644 tests/commands/test_invoke_upload_coverage.py diff --git a/codecov_cli/commands/upload_coverage.py b/codecov_cli/commands/upload_coverage.py new file mode 100644 index 00000000..7f22c998 --- /dev/null +++ b/codecov_cli/commands/upload_coverage.py @@ -0,0 +1,175 @@ +import logging +import pathlib +import typing + +import click + +from codecov_cli.commands.commit import create_commit +from codecov_cli.commands.report import create_report +from codecov_cli.commands.upload import do_upload, global_upload_options +from codecov_cli.helpers.args import get_cli_args +from codecov_cli.helpers.options import global_options +from codecov_cli.services.upload_coverage import upload_coverage_logic +from codecov_cli.types import CommandContext + +logger = logging.getLogger("codecovcli") + + +# These options are the combined options of commit, report and upload commands +@click.command() +@global_options +@global_upload_options +@click.option( + "--parent-sha", + help="SHA (with 40 chars) of what should be the parent of this commit", +) +@click.pass_context +def upload_coverage( + ctx: CommandContext, + branch: typing.Optional[str], + build_code: typing.Optional[str], + build_url: typing.Optional[str], + commit_sha: str, + disable_file_fixes: bool, + disable_search: bool, + dry_run: bool, + env_vars: typing.Dict[str, str], + fail_on_error: bool, + files_search_exclude_folders: typing.List[pathlib.Path], + files_search_explicitly_listed_files: typing.List[pathlib.Path], + files_search_root_folder: pathlib.Path, + flags: typing.List[str], + gcov_args: typing.Optional[str], + gcov_executable: typing.Optional[str], + gcov_ignore: typing.Optional[str], + gcov_include: typing.Optional[str], + git_service: typing.Optional[str], + handle_no_reports_found: bool, + job_code: typing.Optional[str], + name: typing.Optional[str], + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], + network_root_folder: pathlib.Path, + parent_sha: typing.Optional[str], + plugin_names: typing.List[str], + pull_request_number: typing.Optional[str], + report_code: str, + report_type: str, + slug: typing.Optional[str], + swift_project: typing.Optional[str], + token: typing.Optional[str], + use_legacy_uploader: bool, +): + args = get_cli_args(ctx) + logger.debug( + "Starting upload coverage", + extra=dict( + extra_log_attributes=args, + ), + ) + + if not use_legacy_uploader and report_type == "coverage": + versioning_system = ctx.obj["versioning_system"] + codecov_yaml = ctx.obj["codecov_yaml"] or {} + cli_config = codecov_yaml.get("cli", {}) + ci_adapter = ctx.obj.get("ci_adapter") + enterprise_url = ctx.obj.get("enterprise_url") + args = get_cli_args(ctx) + ctx.invoke( + upload_coverage_logic, + cli_config, + versioning_system, + ci_adapter, + branch=branch, + build_code=build_code, + build_url=build_url, + commit_sha=commit_sha, + disable_file_fixes=disable_file_fixes, + disable_search=disable_search, + dry_run=dry_run, + enterprise_url=enterprise_url, + env_vars=env_vars, + fail_on_error=fail_on_error, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, + flags=flags, + gcov_args=gcov_args, + gcov_executable=gcov_executable, + gcov_ignore=gcov_ignore, + gcov_include=gcov_include, + git_service=git_service, + handle_no_reports_found=handle_no_reports_found, + job_code=job_code, + name=name, + network_filter=network_filter, + network_prefix=network_prefix, + network_root_folder=network_root_folder, + parent_sha=parent_sha, + plugin_names=plugin_names, + pull_request_number=pull_request_number, + report_code=report_code, + slug=slug, + swift_project=swift_project, + token=token, + upload_file_type=report_type, + use_legacy_uploader=use_legacy_uploader, + args=args, + ) + else: + ctx.invoke( + create_commit, + commit_sha=commit_sha, + parent_sha=parent_sha, + pull_request_number=pull_request_number, + branch=branch, + slug=slug, + token=token, + git_service=git_service, + fail_on_error=True, + ) + if report_type == "coverage": + ctx.invoke( + create_report, + token=token, + code=report_code, + fail_on_error=True, + commit_sha=commit_sha, + slug=slug, + git_service=git_service, + ) + ctx.invoke( + do_upload, + branch=branch, + build_code=build_code, + build_url=build_url, + commit_sha=commit_sha, + disable_file_fixes=disable_file_fixes, + disable_search=disable_search, + dry_run=dry_run, + env_vars=env_vars, + fail_on_error=fail_on_error, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, + flags=flags, + gcov_args=gcov_args, + gcov_executable=gcov_executable, + gcov_ignore=gcov_ignore, + gcov_include=gcov_include, + git_service=git_service, + handle_no_reports_found=handle_no_reports_found, + job_code=job_code, + name=name, + network_filter=network_filter, + network_prefix=network_prefix, + network_root_folder=network_root_folder, + plugin_names=plugin_names, + pull_request_number=pull_request_number, + report_code=report_code, + report_type=report_type, + slug=slug, + swift_project=swift_project, + token=token, + use_legacy_uploader=use_legacy_uploader, + ) diff --git a/codecov_cli/main.py b/codecov_cli/main.py index 9505aaa6..0640fad8 100644 --- a/codecov_cli/main.py +++ b/codecov_cli/main.py @@ -16,6 +16,7 @@ from codecov_cli.commands.send_notifications import send_notifications from codecov_cli.commands.staticanalysis import static_analysis from codecov_cli.commands.upload import do_upload +from codecov_cli.commands.upload_coverage import upload_coverage from codecov_cli.commands.upload_process import upload_process from codecov_cli.helpers.ci_adapters import get_ci_adapter, get_ci_providers_list from codecov_cli.helpers.config import load_cli_config @@ -74,6 +75,7 @@ def cli( cli.add_command(label_analysis) cli.add_command(static_analysis) cli.add_command(empty_upload) +cli.add_command(upload_coverage) cli.add_command(upload_process) cli.add_command(send_notifications) cli.add_command(process_test_results) diff --git a/codecov_cli/services/upload/__init__.py b/codecov_cli/services/upload/__init__.py index 7b3c884f..003f84bc 100644 --- a/codecov_cli/services/upload/__init__.py +++ b/codecov_cli/services/upload/__init__.py @@ -24,6 +24,7 @@ def do_upload_logic( cli_config: typing.Dict, versioning_system: VersioningSystemInterface, ci_adapter: CIAdapterBase, + upload_coverage: bool = False, *, args: dict = None, branch: typing.Optional[str], @@ -51,6 +52,7 @@ def do_upload_logic( network_filter: typing.Optional[str], network_prefix: typing.Optional[str], network_root_folder: Path, + parent_sha: typing.Optional[str] = None, plugin_names: typing.List[str], pull_request_number: typing.Optional[str], report_code: str, @@ -148,6 +150,8 @@ def do_upload_logic( ci_service, git_service, enterprise_url, + parent_sha, + upload_coverage, args, ) else: diff --git a/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index 84dc8189..6619401b 100644 --- a/codecov_cli/services/upload/upload_sender.py +++ b/codecov_cli/services/upload/upload_sender.py @@ -42,6 +42,8 @@ def send_upload_data( ci_service: typing.Optional[str] = None, git_service: typing.Optional[str] = None, enterprise_url: typing.Optional[str] = None, + parent_sha: typing.Optional[str] = None, + upload_coverage: bool = False, args: dict = None, ) -> RequestResult: data = { @@ -54,6 +56,12 @@ def send_upload_data( "name": name, "version": codecov_cli_version, } + if upload_coverage: + data["branch"] = branch + data["code"] = report_code + data["commitid"] = commit_sha + data["parent_commit_id"] = parent_sha + data["pullid"] = pull_request_number headers = get_token_header(token) encoded_slug = encode_slug(slug) upload_url = enterprise_url or CODECOV_INGEST_URL @@ -66,6 +74,7 @@ def send_upload_data( encoded_slug, commit_sha, report_code, + upload_coverage, ) # Data that goes to storage reports_payload = self._generate_payload( @@ -176,9 +185,14 @@ def get_url_and_possibly_update_data( encoded_slug, commit_sha, report_code, + upload_coverage=False, ): if report_type == "coverage": - url = f"{upload_url}/upload/{git_service}/{encoded_slug}/commits/{commit_sha}/reports/{report_code}/uploads" + base_url = f"{upload_url}/upload/{git_service}/{encoded_slug}" + if upload_coverage: + url = f"{base_url}/upload-coverage" + else: + url = f"{base_url}/commits/{commit_sha}/reports/{report_code}/uploads" elif report_type == "test_results": data["slug"] = encoded_slug data["branch"] = branch diff --git a/codecov_cli/services/upload_coverage/__init__.py b/codecov_cli/services/upload_coverage/__init__.py new file mode 100644 index 00000000..9f53a554 --- /dev/null +++ b/codecov_cli/services/upload_coverage/__init__.py @@ -0,0 +1,90 @@ +import pathlib +import typing + +from codecov_cli.helpers.ci_adapters.base import CIAdapterBase +from codecov_cli.helpers.versioning_systems import VersioningSystemInterface +from codecov_cli.services.upload import do_upload_logic + + +def upload_coverage_logic( + cli_config: typing.Dict, + versioning_system: VersioningSystemInterface, + ci_adapter: CIAdapterBase, + *, + branch: typing.Optional[str], + build_code: typing.Optional[str], + build_url: typing.Optional[str], + commit_sha: str, + disable_file_fixes: bool, + disable_search: bool, + dry_run: bool, + enterprise_url: typing.Optional[str], + env_vars: typing.Dict[str, str], + fail_on_error: bool, + files_search_exclude_folders: typing.List[pathlib.Path], + files_search_explicitly_listed_files: typing.List[pathlib.Path], + files_search_root_folder: pathlib.Path, + flags: typing.List[str], + gcov_args: typing.Optional[str], + gcov_executable: typing.Optional[str], + gcov_ignore: typing.Optional[str], + gcov_include: typing.Optional[str], + git_service: typing.Optional[str], + handle_no_reports_found: bool, + job_code: typing.Optional[str], + name: typing.Optional[str], + network_filter: typing.Optional[str], + network_prefix: typing.Optional[str], + network_root_folder: pathlib.Path, + parent_sha: typing.Optional[str], + plugin_names: typing.List[str], + pull_request_number: typing.Optional[str], + report_code: str, + slug: typing.Optional[str], + swift_project: typing.Optional[str], + token: typing.Optional[str], + use_legacy_uploader: bool, + upload_file_type: str = "coverage", + args: dict = None, +): + return do_upload_logic( + cli_config=cli_config, + versioning_system=versioning_system, + ci_adapter=ci_adapter, + upload_coverage=True, + args=args, + branch=branch, + build_code=build_code, + build_url=build_url, + commit_sha=commit_sha, + disable_file_fixes=disable_file_fixes, + disable_search=disable_search, + dry_run=dry_run, + enterprise_url=enterprise_url, + env_vars=env_vars, + fail_on_error=fail_on_error, + files_search_exclude_folders=files_search_exclude_folders, + files_search_explicitly_listed_files=files_search_explicitly_listed_files, + files_search_root_folder=files_search_root_folder, + flags=flags, + gcov_args=gcov_args, + gcov_executable=gcov_executable, + gcov_ignore=gcov_ignore, + gcov_include=gcov_include, + git_service=git_service, + handle_no_reports_found=handle_no_reports_found, + job_code=job_code, + name=name, + network_filter=network_filter, + network_prefix=network_prefix, + network_root_folder=network_root_folder, + parent_sha=parent_sha, + plugin_names=plugin_names, + pull_request_number=pull_request_number, + report_code=report_code, + slug=slug, + swift_project=swift_project, + token=token, + use_legacy_uploader=use_legacy_uploader, + upload_file_type=upload_file_type, + ) diff --git a/tests/commands/test_invoke_upload_coverage.py b/tests/commands/test_invoke_upload_coverage.py new file mode 100644 index 00000000..3558ed06 --- /dev/null +++ b/tests/commands/test_invoke_upload_coverage.py @@ -0,0 +1,140 @@ +from unittest.mock import patch + +from click.testing import CliRunner + +from codecov_cli.fallbacks import FallbackFieldEnum +from codecov_cli.main import cli +from codecov_cli.types import RequestError, RequestResult +from tests.factory import FakeProvider, FakeVersioningSystem + + +def test_upload_coverage_missing_commit_sha(mocker): + fake_ci_provider = FakeProvider({FallbackFieldEnum.commit_sha: None}) + fake_versioning_system = FakeVersioningSystem({FallbackFieldEnum.commit_sha: None}) + mocker.patch( + "codecov_cli.main.get_versioning_system", return_value=fake_versioning_system + ) + mocker.patch("codecov_cli.main.get_ci_adapter", return_value=fake_ci_provider) + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ["upload-coverage"], obj={}) + assert result.exit_code != 0 + + +def test_upload_coverage_raise_Z_option(mocker, use_verbose_option): + error = RequestError( + code=401, params={"some": "params"}, description="Unauthorized" + ) + command_result = RequestResult( + error=error, warnings=[], status_code=401, text="Unauthorized" + ) + + runner = CliRunner() + with runner.isolated_filesystem(): + with patch( + "codecov_cli.services.commit.send_commit_data" + ) as mocked_create_commit: + mocked_create_commit.return_value = command_result + result = runner.invoke( + cli, + [ + "upload-coverage", + "--fail-on-error", + "-C", + "command-sha", + "--slug", + "owner/repo", + "--report-type", + "test_results", + ], + obj={}, + ) + + assert result.exit_code != 0 + assert "Commit creating failed: Unauthorized" in result.output + assert str(result) == "" + + +def test_upload_coverage_options(mocker): + runner = CliRunner() + fake_ci_provider = FakeProvider({FallbackFieldEnum.commit_sha: None}) + mocker.patch("codecov_cli.main.get_ci_adapter", return_value=fake_ci_provider) + with runner.isolated_filesystem(): + runner = CliRunner() + result = runner.invoke(cli, ["upload-coverage", "-h"], obj={}) + assert result.exit_code == 0 + print(result.output) + + assert result.output.split("\n")[1:] == [ + "Usage: cli upload-coverage [OPTIONS]", + "", + "Options:", + " -C, --sha, --commit-sha TEXT Commit SHA (with 40 chars) [required]", + " -Z, --fail-on-error Exit with non-zero code in case of error", + " --git-service [github|gitlab|bitbucket|github_enterprise|gitlab_enterprise|bitbucket_server]", + " -t, --token TEXT Codecov upload token", + " -r, --slug TEXT owner/repo slug used instead of the private", + " repo token in Self-hosted", + " --code, --report-code TEXT The code of the report. If unsure, leave", + " default", + " --network-root-folder PATH Root folder from which to consider paths on", + " the network section [default: (Current", + " working directory)]", + " -s, --dir, --coverage-files-search-root-folder, --files-search-root-folder PATH", + " Folder where to search for coverage files", + " [default: (Current Working Directory)]", + " --exclude, --coverage-files-search-exclude-folder, --files-search-exclude-folder PATH", + " Folders to exclude from search", + " -f, --file, --coverage-files-search-direct-file, --files-search-direct-file PATH", + " Explicit files to upload. These will be added", + " to the coverage files found for upload. If you", + " wish to only upload the specified files,", + " please consider using --disable-search to", + " disable uploading other files.", + " --disable-search Disable search for coverage files. This is", + " helpful when specifying what files you want to", + " upload with the --file option.", + " --disable-file-fixes Disable file fixes to ignore common lines from", + " coverage (e.g. blank lines or empty brackets)", + " -b, --build, --build-code TEXT Specify the build number manually", + " --build-url TEXT The URL of the build where this is running", + " --job-code TEXT", + " -n, --name TEXT Custom defined name of the upload. Visible in", + " Codecov UI", + " -B, --branch TEXT Branch to which this commit belongs to", + " -P, --pr, --pull-request-number TEXT", + " Specify the pull request number mannually.", + " Used to override pre-existing CI environment", + " variables", + " -e, --env, --env-var TEXT Specify environment variables to be included", + " with this build.", + " -F, --flag TEXT Flag the upload to group coverage metrics.", + " Multiple flags allowed.", + " --plugin TEXT", + " -d, --dry-run Don't upload files to Codecov", + " --legacy, --use-legacy-uploader", + " Use the legacy upload endpoint", + " --handle-no-reports-found Raise no excpetions when no coverage reports", + " found.", + " --report-type [coverage|test_results]", + " The type of the file to upload, coverage by", + " default. Possible values are: testing,", + " coverage.", + " --network-filter TEXT Specify a filter on the files listed in the", + " network section of the Codecov report. This", + " will only add files whose path begin with the", + " specified filter. Useful for upload-specific", + " path fixing", + " --network-prefix TEXT Specify a prefix on files listed in the", + " network section of the Codecov report. Useful", + " to help resolve path fixing", + " --gcov-args TEXT Extra arguments to pass to gcov", + " --gcov-ignore TEXT Paths to ignore during gcov gathering", + " --gcov-include TEXT Paths to include during gcov gathering", + " --gcov-executable TEXT gcov executable to run. Defaults to 'gcov'", + " --swift-project TEXT Specify the swift project", + " --parent-sha TEXT SHA (with 40 chars) of what should be the", + " parent of this commit", + " -h, --help Show this message and exit.", + "", + ] diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 59f70851..47f7e124 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -44,6 +44,8 @@ def test_upload_process_raise_Z_option(mocker, use_verbose_option): "command-sha", "--slug", "owner/repo", + "--report-type", + "test_results", ], obj={}, ) diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 642e2a35..8fe91819 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -80,6 +80,22 @@ def mocked_legacy_upload_endpoint(mocked_responses): yield resp +@pytest.fixture +def mocked_upload_coverage_endpoint(mocked_responses): + encoded_slug = encode_slug(named_upload_data["slug"]) + resp = responses.Response( + responses.POST, + f"https://ingest.codecov.io/upload/github/{encoded_slug}/upload-coverage", + status=200, + json={ + "raw_upload_location": "https://puturl.com", + "url": "https://app.codecov.io/commit-url", + }, + ) + mocked_responses.add(resp) + yield resp + + @pytest.fixture def mocked_test_results_endpoint(mocked_responses): resp = responses.Response( @@ -193,6 +209,31 @@ def test_upload_sender_post_called_with_right_parameters( post_req_made.headers.items() >= headers.items() ) # test dict is a subset of the other + def test_upload_sender_post_called_with_right_parameters_and_upload_coverage( + self, mocked_responses, mocked_upload_coverage_endpoint, mocked_storage_server + ): + headers = {"Authorization": f"token {random_token}"} + + sending_result = UploadSender().send_upload_data( + upload_collection, random_sha, random_token, upload_coverage=True, **named_upload_data + ) + assert sending_result.error is None + assert sending_result.warnings == [] + + assert len(mocked_responses.calls) == 2 + + post_req_made = mocked_responses.calls[0].request + encoded_slug = encode_slug(named_upload_data["slug"]) + response = json.loads(mocked_responses.calls[0].response.text) + assert response.get("url") == "https://app.codecov.io/commit-url" + assert ( + post_req_made.url + == f"https://ingest.codecov.io/upload/github/{encoded_slug}/upload-coverage" + ) + assert ( + post_req_made.headers.items() >= headers.items() + ) # test dict is a subset of the other + def test_upload_sender_post_called_with_right_parameters_test_results( self, mocked_responses, mocked_test_results_endpoint, mocked_storage_server ): diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 9a38a5f9..8de23367 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -125,6 +125,8 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): "git_service", None, None, + False, + None, ) @@ -235,6 +237,8 @@ def test_do_upload_logic_happy_path(mocker): "git_service", None, None, + False, + None, ) @@ -684,5 +688,7 @@ def test_do_upload_logic_happy_path_test_results(mocker): "service", "git_service", None, + None, + False, {"args": "fake_args"}, ) diff --git a/tests/test_codecov_cli.py b/tests/test_codecov_cli.py index 80136f06..6d3a81c3 100644 --- a/tests/test_codecov_cli.py +++ b/tests/test_codecov_cli.py @@ -14,5 +14,6 @@ def test_existing_commands(): "process-test-results", "send-notifications", "static-analysis", + "upload-coverage", "upload-process", ] From c20a2033c56dd63f5ce1f4c19a593ebb9f5a75f6 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:31:01 -0500 Subject: [PATCH 125/128] fix: import error (#567) * fix: import get_cli_args in get_report_results * chore: address linting errors * build: use ruff for linting and run linting this uses ruff for linting, it adds a configuration for ignoring certain rules (F401: unused import) and excluding certain directories (default + languages/ + samples/) * fix: remove as _ from except statements * build: update ci to lint with make lint --- .github/workflows/ci.yml | 12 +-- Makefile | 24 ++++-- codecov_cli/commands/get_report_results.py | 2 + codecov_cli/commands/labelanalysis.py | 8 +- codecov_cli/commands/process_test_results.py | 6 +- codecov_cli/helpers/args.py | 4 +- codecov_cli/helpers/request.py | 5 +- codecov_cli/plugins/pycoverage.py | 1 - codecov_cli/runners/dan_runner.py | 2 +- codecov_cli/runners/pytest_standard_runner.py | 1 - .../services/staticanalysis/__init__.py | 2 +- .../analyzers/python/__init__.py | 1 - .../services/upload/upload_collector.py | 2 +- ruff.toml | 79 +++++++++++++++++++ tests/ci_adapters/test_circleci.py | 2 +- tests/ci_adapters/test_gitlabci.py | 1 - tests/ci_adapters/test_herokuci.py | 10 +-- tests/ci_adapters/test_jenkins.py | 18 +---- tests/ci_adapters/test_local.py | 2 +- tests/commands/test_invoke_labelanalysis.py | 6 +- tests/commands/test_process_test_results.py | 28 +++---- tests/data/reports_examples.py | 1 - tests/helpers/git_services/test_github.py | 2 +- tests/helpers/test_config.py | 4 +- tests/helpers/test_encoder.py | 4 +- tests/helpers/test_git.py | 4 +- tests/helpers/test_request.py | 8 +- tests/helpers/test_upload_sender.py | 2 +- tests/helpers/test_versioning_systems.py | 2 +- .../test_compress_pycoverage_contexts.py | 6 +- tests/services/commit/test_commit_service.py | 2 +- .../static_analysis/test_analyse_file.py | 2 +- .../upload/test_coverage_file_finder.py | 1 - tests/services/upload/test_upload_service.py | 2 +- 34 files changed, 161 insertions(+), 95 deletions(-) create mode 100644 ruff.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e96fda3..434fd283 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,16 +16,10 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - name: Install dependencies + - name: Check linting with ruff run: | - python -m pip install --upgrade pip - pip install black==22.3.0 isort==5.10.1 - - name: Check linting with black - run: | - black --check codecov_cli - - name: Check imports order with isort - run: | - isort --check --profile=black codecov_cli -p staticcodecov_languages + make lint + codecov-startup: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index aed79ccb..96f12b10 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,26 @@ name ?= codecovcli # Semantic versioning format https://semver.org/ tag_regex := ^v([0-9]{1,}\.){2}[0-9]{1,}([-_]\w+)?$ +lint.install: + echo "Installing ruff..." + pip install -Iv ruff + +# The preferred method (for now) w.r.t. fixable rules is to manually update the makefile +# with --fix and re-run 'make lint.' Since ruff is constantly adding rules this is a slight +# amount of "needed" friction imo. +lint.run: + ruff check --ignore F401 --exclude languages --exclude samples + ruff format --exclude languages --exclude samples + +lint.check: + echo "Linting..." + ruff check --ignore F401 --exclude languages --exclude samples + echo "Formatting..." + ruff format --check --exclude languages --exclude samples + lint: - pip install black==22.3.0 isort==5.10.1 - black codecov_cli - isort --profile=black codecov_cli -p staticcodecov_languages - black tests - isort --profile black tests + make lint.install + make lint.run tag.release: ifeq ($(shell echo ${version} | egrep "${tag_regex}"),) diff --git a/codecov_cli/commands/get_report_results.py b/codecov_cli/commands/get_report_results.py index 017025d1..4e02a1f9 100644 --- a/codecov_cli/commands/get_report_results.py +++ b/codecov_cli/commands/get_report_results.py @@ -3,12 +3,14 @@ import click from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum +from codecov_cli.helpers.args import get_cli_args from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.git import GitService from codecov_cli.helpers.options import global_options from codecov_cli.services.report import send_reports_result_get_request from codecov_cli.types import CommandContext + logger = logging.getLogger("codecovcli") diff --git a/codecov_cli/commands/labelanalysis.py b/codecov_cli/commands/labelanalysis.py index cb664994..d384083d 100644 --- a/codecov_cli/commands/labelanalysis.py +++ b/codecov_cli/commands/labelanalysis.py @@ -390,12 +390,12 @@ def _dry_run_list_output( logger.warning(f"label-analysis didn't run correctly. Error: {fallback_reason}") to_run_line = " ".join( - sorted(map(lambda l: f"'{l}'", runner_options)) - + sorted(map(lambda l: f"'{l}'", labels_to_run)) + sorted(map(lambda option: f"'{option}'", runner_options)) + + sorted(map(lambda label: f"'{label}'", labels_to_run)) ) to_skip_line = " ".join( - sorted(map(lambda l: f"'{l}'", runner_options)) - + sorted(map(lambda l: f"'{l}'", labels_to_skip)) + sorted(map(lambda option: f"'{option}'", runner_options)) + + sorted(map(lambda label: f"'{label}'", labels_to_skip)) ) # ⚠️ DON'T use logger # logger goes to stderr, we want it in stdout diff --git a/codecov_cli/commands/process_test_results.py b/codecov_cli/commands/process_test_results.py index 6d3d89f2..b95cc30c 100644 --- a/codecov_cli/commands/process_test_results.py +++ b/codecov_cli/commands/process_test_results.py @@ -103,9 +103,9 @@ def process_test_results( dir, exclude_folders, files, disable_search, report_type="test_results" ) - upload_collection_results: List[ - UploadCollectionResultFile - ] = file_finder.find_files() + upload_collection_results: List[UploadCollectionResultFile] = ( + file_finder.find_files() + ) if len(upload_collection_results) == 0: raise click.ClickException( "No JUnit XML files were found. Make sure to specify them using the --file option." diff --git a/codecov_cli/helpers/args.py b/codecov_cli/helpers/args.py index 74c82f7b..0d797692 100644 --- a/codecov_cli/helpers/args.py +++ b/codecov_cli/helpers/args.py @@ -20,12 +20,12 @@ def get_cli_args(ctx: click.Context): filtered_args = {} for k in args.keys(): try: - if type(args[k]) == PosixPath: + if isinstance(args[k], PosixPath): filtered_args[k] = str(args[k]) else: json.dumps(args[k]) filtered_args[k] = args[k] - except Exception as e: + except Exception: continue return filtered_args diff --git a/codecov_cli/helpers/request.py b/codecov_cli/helpers/request.py index e5c04f8f..27bd3be0 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -52,8 +52,7 @@ def backoff_time(curr_retry): return 2 ** (curr_retry - 1) -class RetryException(Exception): - ... +class RetryException(Exception): ... def retry_request(func): @@ -73,7 +72,7 @@ def wrapper(*args, **kwargs): requests.exceptions.ConnectionError, requests.exceptions.Timeout, RetryException, - ) as exp: + ): logger.warning( "Request failed. Retrying", extra=dict(extra_log_attributes=dict(retry=retry)), diff --git a/codecov_cli/plugins/pycoverage.py b/codecov_cli/plugins/pycoverage.py index 6aba1575..99bbf96b 100644 --- a/codecov_cli/plugins/pycoverage.py +++ b/codecov_cli/plugins/pycoverage.py @@ -54,7 +54,6 @@ def __init__(self, config: dict): self.config = PycoverageConfig(config) def run_preparation(self, collector) -> PreparationPluginReturn: - if shutil.which("coverage") is None: logger.warning("coverage.py is not installed or can't be found.") return diff --git a/codecov_cli/runners/dan_runner.py b/codecov_cli/runners/dan_runner.py index 556c4931..1fee9af5 100644 --- a/codecov_cli/runners/dan_runner.py +++ b/codecov_cli/runners/dan_runner.py @@ -54,7 +54,7 @@ def process_labelanalysis_result(self, result: LabelAnalysisRequestResult): "DAN runner missing 'process_labelanalysis_result_command' configuration value" ) command_list = [] - if type(command) == list: + if isinstance(command, list): command_list.extend(command) else: command_list.append(command) diff --git a/codecov_cli/runners/pytest_standard_runner.py b/codecov_cli/runners/pytest_standard_runner.py index 0ab327cf..fd040429 100644 --- a/codecov_cli/runners/pytest_standard_runner.py +++ b/codecov_cli/runners/pytest_standard_runner.py @@ -57,7 +57,6 @@ def get_available_params(cls) -> List[str]: class PytestStandardRunner(LabelAnalysisRunnerInterface): - dry_run_runner_options = ["--cov-context=test"] params: PytestStandardRunnerConfigParams diff --git a/codecov_cli/services/staticanalysis/__init__.py b/codecov_cli/services/staticanalysis/__init__.py index aedd82c1..3cde4313 100644 --- a/codecov_cli/services/staticanalysis/__init__.py +++ b/codecov_cli/services/staticanalysis/__init__.py @@ -111,7 +111,7 @@ async def run_analysis_entrypoint( failed_uploads = [] with click.progressbar( length=len(files_that_need_upload), - label=f"Upload info to storage", + label="Upload info to storage", ) as bar: # It's better to have less files competing over CPU time when uploading # Especially if we might have large files diff --git a/codecov_cli/services/staticanalysis/analyzers/python/__init__.py b/codecov_cli/services/staticanalysis/analyzers/python/__init__.py index e535698b..d5e6db0c 100644 --- a/codecov_cli/services/staticanalysis/analyzers/python/__init__.py +++ b/codecov_cli/services/staticanalysis/analyzers/python/__init__.py @@ -44,7 +44,6 @@ class PythonAnalyzer(BaseAnalyzer): - condition_statements = [ "if_statement", "while_statement", diff --git a/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 53dcd860..5d0626af 100644 --- a/codecov_cli/services/upload/upload_collector.py +++ b/codecov_cli/services/upload/upload_collector.py @@ -141,7 +141,7 @@ def _get_file_fixes( reason=err.reason, ), ) - except IsADirectoryError as err: + except IsADirectoryError: logger.info(f"Skipping {filename}, found a directory not a file") return UploadCollectionResultFileFixer( diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..42d4e461 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,79 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "languages", + "samples" +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.9 +target-version = "py39" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = ["F401"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" \ No newline at end of file diff --git a/tests/ci_adapters/test_circleci.py b/tests/ci_adapters/test_circleci.py index 6dc7e964..02073448 100644 --- a/tests/ci_adapters/test_circleci.py +++ b/tests/ci_adapters/test_circleci.py @@ -143,7 +143,7 @@ def test_branch(self, env_dict, expected, mocker): assert actual == expected def test_raises_value_error_if_invalid_field(self): - with pytest.raises(ValueError) as ex: + with pytest.raises(ValueError): CircleCICIAdapter().get_fallback_value("some random key x 123") def test_service(self): diff --git a/tests/ci_adapters/test_gitlabci.py b/tests/ci_adapters/test_gitlabci.py index 61e3e3ac..66cd7915 100644 --- a/tests/ci_adapters/test_gitlabci.py +++ b/tests/ci_adapters/test_gitlabci.py @@ -133,7 +133,6 @@ def test_pull_request_number(self, env_dict, expected, mocker): ], ) def test_slug(self, env_dict, expected, mocker): - mocker.patch.dict( os.environ, env_dict, diff --git a/tests/ci_adapters/test_herokuci.py b/tests/ci_adapters/test_herokuci.py index 092ad266..5dc75e30 100644 --- a/tests/ci_adapters/test_herokuci.py +++ b/tests/ci_adapters/test_herokuci.py @@ -73,7 +73,7 @@ def test_branch(self, env_dict, expected, mocker): assert actual == expected def test_raises_value_error_if_invalid_field(self): - with pytest.raises(ValueError) as ex: + with pytest.raises(ValueError): HerokuCIAdapter().get_fallback_value("some_random_key") def test_service(self): @@ -82,7 +82,7 @@ def test_service(self): ) def test_other_values_fallback_to_none(self): - assert HerokuCIAdapter()._get_slug() == None - assert HerokuCIAdapter()._get_build_url() == None - assert HerokuCIAdapter()._get_job_code() == None - assert HerokuCIAdapter()._get_pull_request_number() == None + assert HerokuCIAdapter()._get_slug() is None + assert HerokuCIAdapter()._get_build_url() is None + assert HerokuCIAdapter()._get_job_code() is None + assert HerokuCIAdapter()._get_pull_request_number() is None diff --git a/tests/ci_adapters/test_jenkins.py b/tests/ci_adapters/test_jenkins.py index 69f91454..525f7d3e 100644 --- a/tests/ci_adapters/test_jenkins.py +++ b/tests/ci_adapters/test_jenkins.py @@ -40,18 +40,6 @@ def test_build_url(self, env_dict, expected, mocker): actual = JenkinsAdapter().get_fallback_value(FallbackFieldEnum.build_url) assert actual == expected - @pytest.mark.parametrize( - "env_dict,expected", - [ - ({}, None), - ({JenkinsCIEnvEnum.BUILD_URL: "url"}, "url"), - ], - ) - def test_build_url(self, env_dict, expected, mocker): - mocker.patch.dict(os.environ, env_dict) - actual = JenkinsAdapter().get_fallback_value(FallbackFieldEnum.build_url) - assert actual == expected - @pytest.mark.parametrize( "env_dict,expected", [ @@ -99,6 +87,6 @@ def test_service(self): ) def test_none_values(self): - JenkinsAdapter().get_fallback_value(FallbackFieldEnum.slug) == None - JenkinsAdapter().get_fallback_value(FallbackFieldEnum.commit_sha) == None - JenkinsAdapter().get_fallback_value(FallbackFieldEnum.job_code) == None + JenkinsAdapter().get_fallback_value(FallbackFieldEnum.slug) is None + JenkinsAdapter().get_fallback_value(FallbackFieldEnum.commit_sha) is None + JenkinsAdapter().get_fallback_value(FallbackFieldEnum.job_code) is None diff --git a/tests/ci_adapters/test_local.py b/tests/ci_adapters/test_local.py index d92f1057..36c8022f 100644 --- a/tests/ci_adapters/test_local.py +++ b/tests/ci_adapters/test_local.py @@ -27,7 +27,7 @@ def test_detect_git_not_installed(self, mocker): "codecov_cli.helpers.ci_adapters.local.subprocess.run", return_value=mocker.MagicMock(returncode=1), ) - assert LocalAdapter().detect() == False + assert not LocalAdapter().detect() mocked_subprocess.assert_called_once() @pytest.mark.parametrize( diff --git a/tests/commands/test_invoke_labelanalysis.py b/tests/commands/test_invoke_labelanalysis.py index 230e5b12..729f2965 100644 --- a/tests/commands/test_invoke_labelanalysis.py +++ b/tests/commands/test_invoke_labelanalysis.py @@ -228,7 +228,7 @@ def test_invoke_label_analysis( ): mock_get_runner = get_labelanalysis_deps["mock_get_runner"] fake_runner = get_labelanalysis_deps["fake_runner"] - collected_labels = get_labelanalysis_deps["collected_labels"] + _ = get_labelanalysis_deps["collected_labels"] label_analysis_result = { "present_report_labels": ["test_present"], @@ -349,7 +349,7 @@ def test_invoke_label_analysis_dry_run( def test_invoke_label_analysis_dry_run_pytest_format( self, get_labelanalysis_deps, mocker ): - mock_get_runner = get_labelanalysis_deps["mock_get_runner"] + _ = get_labelanalysis_deps["mock_get_runner"] fake_runner = get_labelanalysis_deps["fake_runner"] label_analysis_result = { @@ -733,7 +733,7 @@ def test_first_labelanalysis_request_fails_but_second_works( ): mock_get_runner = get_labelanalysis_deps["mock_get_runner"] fake_runner = get_labelanalysis_deps["fake_runner"] - collected_labels = get_labelanalysis_deps["collected_labels"] + _ = get_labelanalysis_deps["collected_labels"] label_analysis_result = { "present_report_labels": ["test_present"], diff --git a/tests/commands/test_process_test_results.py b/tests/commands/test_process_test_results.py index 9ef89adc..ea9faaba 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -11,8 +11,7 @@ def test_process_test_results( mocker, tmpdir, ): - - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -21,7 +20,7 @@ def test_process_test_results( "GITHUB_REF": "pull/fake/pull", }, ) - mocked_post = mocker.patch( + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -48,8 +47,7 @@ def test_process_test_results_create_github_message( mocker, tmpdir, ): - - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -95,8 +93,7 @@ def test_process_test_results_update_github_message( mocker, tmpdir, ): - - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -166,8 +163,7 @@ def test_process_test_results_errors_getting_comments( mocker, tmpdir, ): - - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -187,7 +183,7 @@ def test_process_test_results_errors_getting_comments( ), ) - mocked_post = mocker.patch( + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -211,7 +207,7 @@ def test_process_test_results_errors_getting_comments( def test_process_test_results_non_existent_file(mocker, tmpdir): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -220,7 +216,7 @@ def test_process_test_results_non_existent_file(mocker, tmpdir): "GITHUB_REF": "pull/fake/pull", }, ) - mocked_post = mocker.patch( + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -248,7 +244,7 @@ def test_process_test_results_non_existent_file(mocker, tmpdir): def test_process_test_results_missing_repo(mocker, tmpdir): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -258,7 +254,7 @@ def test_process_test_results_missing_repo(mocker, tmpdir): ) if "GITHUB_REPOSITORY" in os.environ: del os.environ["GITHUB_REPOSITORY"] - mocked_post = mocker.patch( + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -288,7 +284,7 @@ def test_process_test_results_missing_repo(mocker, tmpdir): def test_process_test_results_missing_ref(mocker, tmpdir): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + _ = tmpdir.mkdir("folder").join("summary.txt") mocker.patch.dict( os.environ, @@ -299,7 +295,7 @@ def test_process_test_results_missing_ref(mocker, tmpdir): if "GITHUB_REF" in os.environ: del os.environ["GITHUB_REF"] - mocked_post = mocker.patch( + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" diff --git a/tests/data/reports_examples.py b/tests/data/reports_examples.py index 0450a1dd..9c638e43 100644 --- a/tests/data/reports_examples.py +++ b/tests/data/reports_examples.py @@ -2,7 +2,6 @@ # Avoid parsing and removing indentation from multiline strings by defining them in the top level of this file - coverage_file_section_simple = b"""# path=flagtwo.coverage.xml diff --git a/tests/helpers/git_services/test_github.py b/tests/helpers/git_services/test_github.py index 05662766..b77ad832 100644 --- a/tests/helpers/git_services/test_github.py +++ b/tests/helpers/git_services/test_github.py @@ -71,4 +71,4 @@ def mock_request(*args, headers={}, **kwargs): ) slug = "codecov/codecov-cli" response = Github().get_pull_request(slug, 1) - assert response == None + assert response is None diff --git a/tests/helpers/test_config.py b/tests/helpers/test_config.py index 4ec4f513..c3d8d329 100644 --- a/tests/helpers/test_config.py +++ b/tests/helpers/test_config.py @@ -15,13 +15,13 @@ def test_load_config(mocker): def test_load_config_doesnt_exist(mocker): path = pathlib.Path("doesnt/exist") result = load_cli_config(path) - assert result == None + assert result is None def test_load_config_not_file(mocker): path = pathlib.Path("samples/") result = load_cli_config(path) - assert result == None + assert result is None def test_find_codecov_yaml(mocker): diff --git a/tests/helpers/test_encoder.py b/tests/helpers/test_encoder.py index 8e422614..cc185487 100644 --- a/tests/helpers/test_encoder.py +++ b/tests/helpers/test_encoder.py @@ -21,7 +21,7 @@ ], ) def test_encode_invalid_slug(slug): - with pytest.raises(ValueError) as ex: + with pytest.raises(ValueError): encode_slug(slug) @@ -77,7 +77,7 @@ def test_valid_slug(): ) def test_invalid_encoded_slug(slug): assert slug_encoded_incorrectly(slug) - with pytest.raises(ValueError) as ex: + with pytest.raises(ValueError): decode_slug(slug) diff --git a/tests/helpers/test_git.py b/tests/helpers/test_git.py index cca8a60b..77213c19 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -131,5 +131,5 @@ def test_parse_git_service_invalid_service(url): def test_get_git_service_class(): assert isinstance(git.get_git_service("github"), Github) - assert git.get_git_service("gitlab") == None - assert git.get_git_service("bitbucket") == None + assert git.get_git_service("gitlab") is None + assert git.get_git_service("bitbucket") is None diff --git a/tests/helpers/test_request.py b/tests/helpers/test_request.py index 92dd6e6d..7069068b 100644 --- a/tests/helpers/test_request.py +++ b/tests/helpers/test_request.py @@ -38,7 +38,7 @@ def test_log_error_no_raise(mocker): error=error, warnings=[], status_code=401, text="Unauthorized" ) log_warnings_and_errors_if_any(result, "Process", fail_on_error=False) - mock_log_error.assert_called_with(f"Process failed: Unauthorized") + mock_log_error.assert_called_with("Process failed: Unauthorized") def test_log_error_raise(mocker): @@ -51,7 +51,7 @@ def test_log_error_raise(mocker): ) with pytest.raises(SystemExit): log_warnings_and_errors_if_any(result, "Process", fail_on_error=True) - mock_log_error.assert_called_with(f"Process failed: Unauthorized") + mock_log_error.assert_called_with("Process failed: Unauthorized") def test_log_result_without_token(mocker): @@ -137,7 +137,7 @@ def test_request_retry(mocker, valid_response): def test_request_retry_too_many_errors(mocker): - mock_sleep = mocker.patch("codecov_cli.helpers.request.sleep") + _ = mocker.patch("codecov_cli.helpers.request.sleep") mocker.patch.object( requests, "post", @@ -150,7 +150,7 @@ def test_request_retry_too_many_errors(mocker): ], ) with pytest.raises(Exception) as exp: - resp = send_post_request("my_url") + _ = send_post_request("my_url") assert str(exp.value) == "Request failed after too many retries" diff --git a/tests/helpers/test_upload_sender.py b/tests/helpers/test_upload_sender.py index 8fe91819..1e164ef5 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -100,7 +100,7 @@ def mocked_upload_coverage_endpoint(mocked_responses): def mocked_test_results_endpoint(mocked_responses): resp = responses.Response( responses.POST, - f"https://ingest.codecov.io/upload/test_results/v1", + "https://ingest.codecov.io/upload/test_results/v1", status=200, json={ "raw_upload_location": "https://puturl.com", diff --git a/tests/helpers/test_versioning_systems.py b/tests/helpers/test_versioning_systems.py index cc74410f..52d13793 100644 --- a/tests/helpers/test_versioning_systems.py +++ b/tests/helpers/test_versioning_systems.py @@ -128,5 +128,5 @@ def test_list_relevant_files_fails_if_no_root_is_found(self, mocker): ) vs = GitVersioningSystem() - with pytest.raises(ValueError) as ex: + with pytest.raises(ValueError): vs.list_relevant_files() diff --git a/tests/plugins/test_compress_pycoverage_contexts.py b/tests/plugins/test_compress_pycoverage_contexts.py index 1dc8fc86..fe89a99f 100644 --- a/tests/plugins/test_compress_pycoverage_contexts.py +++ b/tests/plugins/test_compress_pycoverage_contexts.py @@ -172,7 +172,7 @@ class TestCompressPycoverageContexts(object): def test_default_options(self): plugin = CompressPycoverageContexts() assert plugin.config.file_to_compress == pathlib.Path("coverage.json") - assert plugin.config.delete_uncompressed == True + assert plugin.config.delete_uncompressed assert plugin.file_to_compress == pathlib.Path("coverage.json") assert plugin.file_to_write == pathlib.Path("coverage.codecov.json") @@ -183,7 +183,7 @@ def test_change_options(self): } plugin = CompressPycoverageContexts(config) assert plugin.config.file_to_compress == pathlib.Path("label.coverage.json") - assert plugin.config.delete_uncompressed == False + assert not plugin.config.delete_uncompressed assert plugin.file_to_compress == pathlib.Path("label.coverage.json") assert plugin.file_to_write == pathlib.Path("label.coverage.codecov.json") @@ -192,7 +192,7 @@ def test_run_preparation_fail_fast_no_file(self): res = plugin.run_preparation(None) assert res == PreparationPluginReturn( success=False, - messages=[f"File to compress coverage.json not found."], + messages=["File to compress coverage.json not found."], ) def test_run_preparation_fail_fast_path_not_file(self, tmp_path): diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 2fd5157d..90af6ddf 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -156,7 +156,7 @@ def test_commit_sender_with_forked_repo(mocker): ) mocker.patch("os.environ", dict(TOKENLESS="user_forked_repo/codecov-cli:branch")) - res = send_commit_data( + _ = send_commit_data( "commit_sha", "parent_sha", "1", diff --git a/tests/services/static_analysis/test_analyse_file.py b/tests/services/static_analysis/test_analyse_file.py index 7a31e864..43269b16 100644 --- a/tests/services/static_analysis/test_analyse_file.py +++ b/tests/services/static_analysis/test_analyse_file.py @@ -53,6 +53,6 @@ def test_analyse_file_no_analyzer(mock_get_analyzer, mock_open): mock_open.return_value.__enter__.return_value.read.return_value = fake_contents config = {} res = analyze_file(config, file_name) - assert res == None + assert res is None mock_open.assert_called_with("filepath", "rb") mock_get_analyzer.assert_called_with(file_name, fake_contents) diff --git a/tests/services/upload/test_coverage_file_finder.py b/tests/services/upload/test_coverage_file_finder.py index 29261fa1..201701c8 100644 --- a/tests/services/upload/test_coverage_file_finder.py +++ b/tests/services/upload/test_coverage_file_finder.py @@ -357,7 +357,6 @@ def test_find_coverage_files_with_user_specified_files_not_found( def test_find_coverage_files_with_user_specified_files_in_default_ignored_folder( self, coverage_file_finder_fixture ): - ( project_root, coverage_file_finder, diff --git a/tests/services/upload/test_upload_service.py b/tests/services/upload/test_upload_service.py index 8de23367..2cf216d1 100644 --- a/tests/services/upload/test_upload_service.py +++ b/tests/services/upload/test_upload_service.py @@ -533,7 +533,7 @@ def side_effect(*args, **kwargs): ci_adapter.get_fallback_value.return_value = "service" with pytest.raises(click.ClickException) as exp: - res = do_upload_logic( + _ = do_upload_logic( cli_config, versioning_system, ci_adapter, From 87d5916c61ce4eca32679e25c713259eefac221e Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 2 Dec 2024 20:22:13 +0100 Subject: [PATCH 126/128] Take Java JUnit conventions into account when detecting test files to upload (#550) --- codecov_cli/services/upload/file_finder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index 9c0a89cc..232dde41 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -38,6 +38,8 @@ test_results_files_patterns = [ "*junit*.xml", "*test*.xml", + # the actual JUnit (Java) prefixes the tests with "TEST-" + "*TEST-*.xml" ] coverage_files_excluded_patterns = [ From 193e4beeabd5efa9d975b2f9d91bb642f740573f Mon Sep 17 00:00:00 2001 From: Mathieu Rochette Date: Tue, 3 Dec 2024 17:38:36 +0100 Subject: [PATCH 127/128] fix typo (#546) * fix typo * fix: update test --------- Co-authored-by: Tom Hu <88201630+thomasrockhu-codecov@users.noreply.github.com> Co-authored-by: Tom Hu --- codecov_cli/commands/upload.py | 2 +- codecov_cli/services/upload/file_finder.py | 2 +- tests/commands/test_invoke_upload_coverage.py | 2 +- tests/commands/test_invoke_upload_process.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codecov_cli/commands/upload.py b/codecov_cli/commands/upload.py index 2b78f687..7d67ff30 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -162,7 +162,7 @@ def _turn_env_vars_into_dict(ctx, params, value): "--handle-no-reports-found", "handle_no_reports_found", is_flag=True, - help="Raise no excpetions when no coverage reports found.", + help="Raise no exceptions when no coverage reports found.", ), click.option( "--report-type", diff --git a/codecov_cli/services/upload/file_finder.py b/codecov_cli/services/upload/file_finder.py index 232dde41..f03745df 100644 --- a/codecov_cli/services/upload/file_finder.py +++ b/codecov_cli/services/upload/file_finder.py @@ -39,7 +39,7 @@ "*junit*.xml", "*test*.xml", # the actual JUnit (Java) prefixes the tests with "TEST-" - "*TEST-*.xml" + "*TEST-*.xml", ] coverage_files_excluded_patterns = [ diff --git a/tests/commands/test_invoke_upload_coverage.py b/tests/commands/test_invoke_upload_coverage.py index 3558ed06..bb620ec6 100644 --- a/tests/commands/test_invoke_upload_coverage.py +++ b/tests/commands/test_invoke_upload_coverage.py @@ -114,7 +114,7 @@ def test_upload_coverage_options(mocker): " -d, --dry-run Don't upload files to Codecov", " --legacy, --use-legacy-uploader", " Use the legacy upload endpoint", - " --handle-no-reports-found Raise no excpetions when no coverage reports", + " --handle-no-reports-found Raise no exceptions when no coverage reports", " found.", " --report-type [coverage|test_results]", " The type of the file to upload, coverage by", diff --git a/tests/commands/test_invoke_upload_process.py b/tests/commands/test_invoke_upload_process.py index 47f7e124..c5490e3b 100644 --- a/tests/commands/test_invoke_upload_process.py +++ b/tests/commands/test_invoke_upload_process.py @@ -114,7 +114,7 @@ def test_upload_process_options(mocker): " -d, --dry-run Don't upload files to Codecov", " --legacy, --use-legacy-uploader", " Use the legacy upload endpoint", - " --handle-no-reports-found Raise no excpetions when no coverage reports", + " --handle-no-reports-found Raise no exceptions when no coverage reports", " found.", " --report-type [coverage|test_results]", " The type of the file to upload, coverage by", From ceeaddc68c00d26a4ffb7c043f1d562c4c9c6f27 Mon Sep 17 00:00:00 2001 From: joseph-sentry <136376984+joseph-sentry@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:04:58 -0500 Subject: [PATCH 128/128] fix: add log messages and tests for tokenless (#475) --- codecov_cli/services/commit/__init__.py | 22 +++++++----- tests/services/commit/test_commit_service.py | 37 +++++++++++++++++--- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/codecov_cli/services/commit/__init__.py b/codecov_cli/services/commit/__init__.py index 54518de7..a872e1d1 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -53,15 +53,21 @@ def send_commit_data( enterprise_url, args, ): - # this is how the CLI receives the username of the user to whom the fork belongs - # to and the branch name from the action - tokenless = os.environ.get("TOKENLESS") - if tokenless: - headers = None # type: ignore - branch = tokenless # type: ignore - logger.info("The PR is happening in a forked repo. Using tokenless upload.") + # Old versions of the GHA use this env var instead of the regular branch + # argument to provide an unprotected branch name + if tokenless := os.environ.get("TOKENLESS"): + branch = tokenless + + if branch and ":" in branch: + logger.info(f"Creating a commit for an unprotected branch: {branch}") + elif token is None: + logger.warning( + f"Branch `{branch}` is protected but no token was provided\nFor information on Codecov upload tokens, see https://docs.codecov.com/docs/codecov-tokens" + ) else: - headers = get_token_header(token) + logger.info("Using token to create a commit for protected branch `{branch}`") + + headers = get_token_header(token) data = { "branch": branch, diff --git a/tests/services/commit/test_commit_service.py b/tests/services/commit/test_commit_service.py index 90af6ddf..362cb48e 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -1,9 +1,6 @@ -import json import uuid -import requests from click.testing import CliRunner -from requests import Response from codecov_cli.services.commit import create_commit_logic, send_commit_data from codecov_cli.types import RequestError, RequestResult, RequestResultWarning @@ -155,12 +152,11 @@ def test_commit_sender_with_forked_repo(mocker): return_value=mocker.MagicMock(status_code=200, text="success"), ) - mocker.patch("os.environ", dict(TOKENLESS="user_forked_repo/codecov-cli:branch")) _ = send_commit_data( "commit_sha", "parent_sha", "1", - "branch", + "user_forked_repo/codecov-cli:branch", "codecov::::codecov-cli", None, "github", @@ -208,3 +204,34 @@ def test_commit_without_token(mocker): }, headers=None, ) + + +def test_commit_sender_with_forked_repo_bad_branch(mocker): + mocked_response = mocker.patch( + "codecov_cli.services.commit.send_post_request", + return_value=mocker.MagicMock(status_code=200, text="success"), + ) + mocker.patch("os.environ", dict(TOKENLESS="user_forked_repo/codecov-cli:branch")) + _res = send_commit_data( + "commit_sha", + "parent_sha", + "1", + "branch", + "codecov::::codecov-cli", + None, + "github", + None, + None, + ) + + mocked_response.assert_called_with( + url="https://ingest.codecov.io/upload/github/codecov::::codecov-cli/commits", + data={ + "branch": "user_forked_repo/codecov-cli:branch", + "cli_args": None, + "commitid": "commit_sha", + "parent_commit_id": "parent_sha", + "pullid": "1", + }, + headers=None, + )