diff --git a/.github/workflows/build_for_pypi.yml b/.github/workflows/build_for_pypi.yml index 05d9793e..1315f24a 100644 --- a/.github/workflows/build_for_pypi.yml +++ b/.github/workflows/build_for_pypi.yml @@ -36,6 +36,6 @@ jobs: - name: Publish package to PyPi if: inputs.publish == true uses: pypa/gh-action-pypi-publish@release/v1 - - - + with: + attestations: false + verbose: true 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/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/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 f9cf8ab1..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)), @@ -102,7 +101,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 +112,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/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/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/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..da2b5127 100644 --- a/codecov_cli/services/report/__init__.py +++ b/codecov_cli/services/report/__init__.py @@ -1,15 +1,15 @@ import json import logging 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 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,11 +24,11 @@ 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, - args: dict = None, + args: typing.Union[dict, None] = None, ): encoded_slug = encode_slug(slug) sending_result = send_create_report_request( @@ -70,10 +70,10 @@ 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, + args: typing.Union[dict, None] = None, ): encoded_slug = encode_slug(slug) sending_result = send_reports_result_request( @@ -83,6 +83,7 @@ def create_report_results_logic( service=service, token=token, enterprise_url=enterprise_url, + args=args, ) log_warnings_and_errors_if_any( @@ -103,7 +104,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 +119,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/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/__init__.py b/codecov_cli/services/upload/__init__.py index ae1a22a2..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,12 +52,13 @@ 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, 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, ): @@ -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/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 = [ 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/codecov_cli/services/upload/upload_sender.py b/codecov_cli/services/upload/upload_sender.py index bfd5a07f..6619401b 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", @@ -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/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/setup.py b/setup.py index b1c59f32..4ffee44c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.8.0", + version="0.9.4", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, @@ -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.*", 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_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 2d1f3550..c5490e3b 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/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 642e2a35..1e164ef5 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -80,11 +80,27 @@ 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( 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", @@ -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/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_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..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", @@ -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..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", @@ -108,6 +110,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 +142,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 +162,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 +200,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 +221,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 +239,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/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 9a38a5f9..2cf216d1 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, ) @@ -529,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, @@ -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/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", 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", ]