diff --git a/codecov_cli/commands/upload_coverage.py b/codecov_cli/commands/upload_coverage.py new file mode 100644 index 000000000..7f22c9989 --- /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 9505aaa63..0640fad87 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 7b3c884fb..003f84bc2 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 84dc8189f..6619401bf 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 000000000..9f53a5540 --- /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 000000000..3558ed06f --- /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 59f70851a..47f7e1243 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 642e2a350..8fe918190 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 9a38a5f98..8de233677 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 80136f06f..6d3a81c34 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", ]