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-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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9d26e76..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 @@ -76,12 +70,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: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: ${{matrix.python-version}}junit.xml + path: ${{matrix.python-version}}junit.xml static-analysis: runs-on: ubuntu-latest @@ -118,10 +118,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) @@ -131,38 +133,32 @@ 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() && github.ref && contains(github.ref, 'pull') }} + run: | + codecovcli process-test-results --dir test_results --github-token ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index f70eab15..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}"),) @@ -16,4 +30,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/README.md b/README.md index 1a09683b..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: - -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 | +| --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/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 f887c5b9..b95cc30c 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: + 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/commands/upload.py b/codecov_cli/commands/upload.py index dfbfe431..2b78f687 100644 --- a/codecov_cli/commands/upload.py +++ b/codecov_cli/commands/upload.py @@ -178,6 +178,26 @@ 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'", + ), + click.option( + "--swift-project", + help="Specify the swift project", + ), ] @@ -207,6 +227,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], @@ -218,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, ): @@ -251,6 +276,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, @@ -262,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_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/commands/upload_process.py b/codecov_cli/commands/upload_process.py index e6efecc2..1ee77eca 100644 --- a/codecov_cli/commands/upload_process.py +++ b/codecov_cli/commands/upload_process.py @@ -25,34 +25,39 @@ @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], + swift_project: typing.Optional[str], + token: typing.Optional[str], + use_legacy_uploader: bool, ): args = get_cli_args(ctx) logger.debug( @@ -85,31 +90,36 @@ 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, + 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/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/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/helpers/request.py b/codecov_cli/helpers/request.py index 17af81d1..27bd3be0 100644 --- a/codecov_cli/helpers/request.py +++ b/codecov_cli/helpers/request.py @@ -94,7 +94,17 @@ def send_post_request( return request_result(post(url=url, data=data, headers=headers, params=params)) -def get_token_header_or_fail(token: str | None) -> dict: +@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: 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." @@ -102,7 +112,10 @@ def get_token_header_or_fail(token: str | None) -> dict: return {"Authorization": f"token {token}"} -def get_token_header(token: str | None) -> 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}"} @@ -117,7 +130,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, 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/__init__.py b/codecov_cli/plugins/__init__.py index 2ce8cb6d..db7a8a4d 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,16 +64,25 @@ 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) 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/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/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/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/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 0130c1cc..cee0fefb 100644 --- a/codecov_cli/services/commit/__init__.py +++ b/codecov_cli/services/commit/__init__.py @@ -2,8 +2,8 @@ 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.config import CODECOV_INGEST_URL +from codecov_cli.helpers.encoder import encode_slug from codecov_cli.helpers.request import ( get_token_header, log_warnings_and_errors_if_any, @@ -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, @@ -53,15 +53,6 @@ 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 - - if not token: - if branch and ":" in branch: - logger.info("Creating a commit on an unprotected branch") - else: - logger.warning("Token is missing, but branch is missing or protected") - headers = get_token_header(token) data = { @@ -72,7 +63,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/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 a3734ad1..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 -from codecov_cli.helpers.encoder import decode_slug, encode_slug +from codecov_cli.helpers.config import CODECOV_API_URL, CODECOV_INGEST_URL +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( @@ -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) @@ -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 411c820c..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], @@ -40,6 +41,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], @@ -47,16 +52,29 @@ 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], - token: str, + swift_project: typing.Optional[str], + token: typing.Optional[str], 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, + "swift_project": swift_project, + } 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 +91,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) @@ -128,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 a0a1db45..232dde41 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 @@ -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 = [ @@ -183,9 +185,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 +197,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 +205,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 +228,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/codecov_cli/services/upload/upload_collector.py b/codecov_cli/services/upload/upload_collector.py index 47a99670..5d0626af 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] @@ -139,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 22f8924a..6619401b 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, @@ -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,9 +56,15 @@ 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_API_URL + upload_url = enterprise_url or CODECOV_INGEST_URL url, data = self.get_url_and_possibly_update_data( data, upload_file_type, @@ -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/requirements.txt b/requirements.txt index d40822af..2d33a3d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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/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 bb92b319..4ffee44c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="codecov-cli", - version="0.7.4", + version="0.9.4", packages=find_packages(exclude=["contrib", "docs", "tests*"]), description="Codecov Command Line Interface", long_description=long_description, @@ -19,12 +19,12 @@ author_email="support@codecov.io", install_requires=[ "click==8.*", - "httpx==0.23.*", + "httpx==0.27.*", "ijson==3.*", "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/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/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 ec3981b2..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={}, ) @@ -126,6 +128,11 @@ 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'", + " --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_process_test_results.py b/tests/commands/test_process_test_results.py index b6e7eaf8..ea9faaba 100644 --- a/tests/commands/test_process_test_results.py +++ b/tests/commands/test_process_test_results.py @@ -1,3 +1,4 @@ +import json import os from click.testing import CliRunner @@ -10,16 +11,57 @@ 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, { "GITHUB_REPOSITORY": "fake/repo", "GITHUB_REF": "pull/fake/pull", - "GITHUB_STEP_SUMMARY": tmp_file.dirname + tmp_file.basename, }, ) + _ = 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", + "--file", + "samples/junit.xml", + "--disable-search", + ], + obj={}, + ) + + assert result.exit_code == 0 + # Ensure that there's an output + assert result.output + + +def test_process_test_results_create_github_message( + mocker, + tmpdir, +): + _ = 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( @@ -31,8 +73,8 @@ def test_process_test_results( cli, [ "process-test-results", - "--provider-token", - "whatever", + "--github-token", + "fake-token", "--file", "samples/junit.xml", "--disable-search", @@ -41,44 +83,107 @@ def test_process_test_results( ) assert result.exit_code == 0 + assert ( + mocked_post.call_args.kwargs["url"] + == "https://api.github.com/repos/fake/repo/issues/123/comments" + ) - 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():
> 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
.../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": (), - }, + +def test_process_test_results_update_github_message( + mocker, + tmpdir, +): + _ = tmpdir.mkdir("folder").join("summary.txt") + + mocker.patch.dict( + os.environ, + { + "GITHUB_REPOSITORY": "fake/repo", + "GITHUB_REF": "pull/fake/123", }, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": "Bearer whatever", - "X-GitHub-Api-Version": "2022-11-28", + ) + + 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( + 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/comments/12345" + ) -def test_process_test_results_non_existent_file(mocker, tmpdir): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") + +def test_process_test_results_errors_getting_comments( + mocker, + tmpdir, +): + _ = 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", }, ) - mocked_post = mocker.patch( + + mocker.patch( + "codecov_cli.commands.process_test_results.send_get_request", + return_value=RequestResult( + status_code=400, + error=None, + warnings=[], + text="", + ), + ) + + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -89,37 +194,29 @@ 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 == 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") +def test_process_test_results_non_existent_file(mocker, tmpdir): + _ = 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( + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -130,10 +227,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={}, @@ -142,26 +237,24 @@ 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): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") +def test_process_test_results_missing_repo(mocker, tmpdir): + _ = 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"] - mocked_post = mocker.patch( + if "GITHUB_REPOSITORY" in os.environ: + del os.environ["GITHUB_REPOSITORY"] + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -172,7 +265,7 @@ def test_process_test_results_missing_ref(mocker, tmpdir): cli, [ "process-test-results", - "--provider-token", + "--github-token", "whatever", "--file", "samples/junit.xml", @@ -184,25 +277,25 @@ 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): - tmp_file = tmpdir.mkdir("folder").join("summary.txt") +def test_process_test_results_missing_ref(mocker, tmpdir): + _ = 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( + + if "GITHUB_REF" in os.environ: + del os.environ["GITHUB_REF"] + _ = mocker.patch( "codecov_cli.commands.process_test_results.send_post_request", return_value=RequestResult( status_code=200, error=None, warnings=[], text="yay it worked" @@ -213,7 +306,7 @@ def test_process_test_results_missing_step_summary(mocker, tmpdir): cli, [ "process-test-results", - "--provider-token", + "--github-token", "whatever", "--file", "samples/junit.xml", @@ -225,7 +318,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 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 fdcc778e..77213c19 100644 --- a/tests/helpers/test_git.py +++ b/tests/helpers/test_git.py @@ -131,7 +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_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..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): @@ -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() @@ -132,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", @@ -145,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 9033082c..1e164ef5 100644 --- a/tests/helpers/test_upload_sender.py +++ b/tests/helpers/test_upload_sender.py @@ -69,7 +69,23 @@ 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", + "url": "https://app.codecov.io/commit-url", + }, + ) + mocked_responses.add(resp) + 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", @@ -84,7 +100,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", + "https://ingest.codecov.io/upload/test_results/v1", status=200, json={ "raw_upload_location": "https://puturl.com", @@ -187,7 +203,32 @@ 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() + ) # 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() @@ -217,7 +258,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 +295,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() @@ -301,10 +342,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/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/plugins/test_instantiation.py b/tests/plugins/test_instantiation.py index caaa3516..fdf3a842 100644 --- a/tests/plugins/test_instantiation.py +++ b/tests/plugins/test_instantiation.py @@ -106,40 +106,50 @@ 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 +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_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 c8a43f8b..ebcab02e 100644 --- a/tests/services/commit/test_commit_service.py +++ b/tests/services/commit/test_commit_service.py @@ -152,7 +152,8 @@ def test_commit_sender_with_forked_repo(mocker): return_value=mocker.MagicMock(status_code=200, text="success"), ) - res = send_commit_data( + mocker.patch("os.environ", dict(TOKENLESS="user_forked_repo/codecov-cli:branch")) + _ = send_commit_data( "commit_sha", "parent_sha", "1", @@ -164,7 +165,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, @@ -176,13 +177,13 @@ def test_commit_sender_with_forked_repo(mocker): ) -def test_commit_sender_with_forked_repo_bad_branch(mocker): +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"), ) - _res = send_commit_data( + send_commit_data( "commit_sha", "parent_sha", "1", @@ -193,9 +194,8 @@ def test_commit_sender_with_forked_repo_bad_branch(mocker): None, 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": "branch", "cli_args": None, diff --git a/tests/services/empty_upload/test_empty_upload.py b/tests/services/empty_upload/test_empty_upload.py index 16f9f946..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 @@ -21,7 +23,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 +59,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()) @@ -133,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 b3a0f04a..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( @@ -124,5 +144,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_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/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..201701c8 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 @@ -353,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_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 4bba13e9..2cf216d1 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, @@ -67,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, @@ -81,10 +86,25 @@ 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, + "swift_project": "App", + }, ) 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, @@ -105,6 +125,8 @@ def test_do_upload_logic_happy_path_legacy_uploader(mocker): "git_service", None, None, + False, + None, ) @@ -147,6 +169,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, @@ -158,6 +184,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, @@ -171,10 +198,25 @@ 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, + "swift_project": "App", + }, ) 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, @@ -195,6 +237,8 @@ def test_do_upload_logic_happy_path(mocker): "git_service", None, None, + False, + None, ) @@ -233,6 +277,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, @@ -244,6 +292,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", @@ -251,11 +300,26 @@ 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( - 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, + "swift_project": "App", + }, ) assert out_bytes == [ ("info", "dry-run option activated. NOT sending data to Codecov."), @@ -288,30 +352,35 @@ 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", + swift_project="App", + 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 == [ @@ -373,6 +442,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, @@ -384,6 +457,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, @@ -403,10 +477,25 @@ 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, + "swift_project": "App", + }, ) 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", @@ -444,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, @@ -456,6 +545,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, @@ -467,6 +560,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, @@ -477,10 +571,25 @@ 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, + "swift_project": "App", + }, ) 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") @@ -515,29 +624,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 == [ @@ -549,7 +663,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 +688,7 @@ def test_do_upload_logic_happy_path_test_results(mocker): "service", "git_service", None, - {"args": "fake_args"} + 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", ]