Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: UI and status report diff patch numbers #679

Merged
merged 3 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions helpers/comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from shared.reports.types import ReportTotals


def minimal_totals(totals: ReportTotals | None) -> dict:
if totals is None:
return {
"hits": 0,
"misses": 0,
"partials": 0,
"coverage": None,
}
return {
"hits": totals.hits,
"misses": totals.misses,
"partials": totals.partials,
# ReportTotals has coverage as a string, we want float in the DB
# Also the coverage from ReportTotals is 0-100, while in the DB it's 0-1
"coverage": (
(float(totals.coverage) / 100) if totals.coverage is not None else None
),
}
20 changes: 18 additions & 2 deletions services/comparison/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
self._repository_service = None
self._adjusted_base_diff = None
self._original_base_diff = None
self._patch_totals = None
self._changes = None
self._existing_statuses = None
self._behind_by = None
Expand Down Expand Up @@ -209,9 +210,11 @@

Patch coverage refers to looking at the coverage in HEAD report filtered by the git diff HEAD..BASE.
"""
if self._patch_totals:
return self._patch_totals
diff = await self.get_diff(use_original_base=True)
totals = self.head.report.apply_diff(diff)
return totals
self._patch_totals = self.head.report.apply_diff(diff)
return self._patch_totals

async def get_behind_by(self):
async with self._behind_by_lock:
Expand Down Expand Up @@ -371,6 +374,7 @@
self.flags = flags
self.path_patterns = path_patterns
self.real_comparison = real_comparison
self._patch_totals = None
self._changes = None
self.project_coverage_base = FullCommit(
commit=real_comparison.project_coverage_base.commit,
Expand All @@ -394,6 +398,18 @@
async def get_diff(self, use_original_base=False):
return await self.real_comparison.get_diff(use_original_base=use_original_base)

@sentry_sdk.trace
async def get_patch_totals(self) -> ReportTotals | None:
"""Returns the patch coverage for the comparison.

Patch coverage refers to looking at the coverage in HEAD report filtered by the git diff HEAD..BASE.
"""
if self._patch_totals:
return self._patch_totals

Check warning on line 408 in services/comparison/__init__.py

View check run for this annotation

Codecov Notifications / codecov/patch

services/comparison/__init__.py#L408

Added line #L408 was not covered by tests
diff = await self.get_diff(use_original_base=True)
self._patch_totals = self.head.report.apply_diff(diff)
return self._patch_totals

async def get_existing_statuses(self):
return await self.real_comparison.get_existing_statuses()

Expand Down
7 changes: 4 additions & 3 deletions services/notification/notifiers/mixins/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@


class StatusPatchMixin(object):
async def get_patch_status(self, comparison) -> Tuple[str, str]:
async def get_patch_status(
self, comparison: ComparisonProxy | FilteredComparison
) -> Tuple[str, str]:
threshold = self.notifier_yaml_settings.get("threshold", "0.0")

# check if user has erroneously added a % to this input and fix
Expand All @@ -21,8 +23,7 @@ async def get_patch_status(self, comparison) -> Tuple[str, str]:
except (InvalidOperation, TypeError):
threshold = Decimal("0.0")

diff = await comparison.get_diff(use_original_base=True)
totals = comparison.head.report.apply_diff(diff)
totals = await comparison.get_patch_totals()
if self.notifier_yaml_settings.get("target") not in ("auto", None):
target_coverage = Decimal(
str(self.notifier_yaml_settings.get("target")).replace("%", "")
Expand Down
1 change: 1 addition & 0 deletions services/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ class EnrichedPull(object):
provider_pull: Optional[Mapping[str, Any]]


@sentry_sdk.trace
async def fetch_and_update_pull_request_information_from_commit(
repository_service, commit, current_yaml
) -> Optional[EnrichedPull]:
Expand Down
38 changes: 4 additions & 34 deletions tasks/compute_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
from shared.components import Component
from shared.helpers.flag import Flag
from shared.reports.readonly import ReadOnlyReport
from shared.reports.types import ReportTotals
from shared.torngit.exceptions import TorngitRateLimitError
from shared.yaml import UserYaml

from app import celery_app
from database.enums import CompareCommitError, CompareCommitState
from database.models import CompareCommit, CompareComponent, CompareFlag
from database.models.reports import ReportLevelTotals, RepositoryFlag
from helpers.comparison import minimal_totals
from helpers.github_installation import get_installation_name_for_owner_for_task
from services.archive import ArchiveService
from services.comparison import ComparisonContext, ComparisonProxy, FilteredComparison
Expand Down Expand Up @@ -66,8 +66,9 @@ def run_impl(

# At this point we can calculate the patch coverage
# Because we have a HEAD report and a base commit to get the diff from
patch_totals = async_to_sync(comparison_proxy.get_patch_totals)()
comparison.patch_totals = self.minimal_totals(patch_totals)
if comparison.patch_totals is None:
patch_totals = async_to_sync(comparison_proxy.get_patch_totals)()
comparison.patch_totals = minimal_totals(patch_totals)

if not comparison_proxy.has_project_coverage_base_report():
comparison.error = CompareCommitError.missing_base_report.value
Expand Down Expand Up @@ -96,18 +97,6 @@ def run_impl(
path = self.store_results(comparison, impacted_files)

comparison.report_storage_path = path
calculated_patch_totals = impacted_files.get("changes_summary").get(
"patch_totals"
)
if calculated_patch_totals != comparison.patch_totals:
log.warning(
"Calculated patch totals differ!",
extra={
**log_extra,
"calculated_patch_totals": calculated_patch_totals,
"patch_totals": comparison.patch_totals,
},
)

comparison.state = CompareCommitState.processed.value
log.info("Computing comparison successful", extra=log_extra)
Expand All @@ -119,25 +108,6 @@ def run_impl(
db_session.commit()
return {"successful": True}

def minimal_totals(self, totals: ReportTotals | None) -> dict:
if totals is None:
return {
"hits": 0,
"misses": 0,
"partials": 0,
"coverage": None,
}
return {
"hits": totals.hits,
"misses": totals.misses,
"partials": totals.partials,
# ReportTotals has coverage as a string, we want float in the DB
# Also the coverage from ReportTotals is 0-100, while in the DB it's 0-1
"coverage": (
(float(totals.coverage) / 100) if totals.coverage is not None else None
),
}

def compute_flag_comparison(self, db_session, comparison, comparison_proxy):
log_extra = dict(comparison_id=comparison.id)
log.info("Computing flag comparisons", extra=log_extra)
Expand Down
35 changes: 34 additions & 1 deletion tasks/notify.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from typing import Optional

import sentry_sdk
from asgiref.sync import async_to_sync
from celery.exceptions import MaxRetriesExceededError, SoftTimeLimitExceeded
from shared.celery_config import (
Expand All @@ -14,15 +15,17 @@
from shared.torngit.base import TokenType, TorngitBaseAdapter
from shared.torngit.exceptions import TorngitClientError, TorngitServerFailureError
from shared.yaml import UserYaml
from sqlalchemy import and_
from sqlalchemy.orm.session import Session

from app import celery_app
from database.enums import CommitErrorTypes, Decoration, NotificationState, ReportType
from database.models import Commit, Pull
from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME
from database.models.core import GITHUB_APP_INSTALLATION_DEFAULT_NAME, CompareCommit
from helpers.checkpoint_logger import from_kwargs as checkpoints_from_kwargs
from helpers.checkpoint_logger.flows import UploadFlow
from helpers.clock import get_seconds_to_next_hour
from helpers.comparison import minimal_totals
from helpers.exceptions import NoConfiguredAppsAvailable, RepositoryWithoutValidBotError
from helpers.github_installation import get_installation_name_for_owner_for_task
from helpers.save_commit_error import save_commit_error
Expand Down Expand Up @@ -511,6 +514,33 @@ def has_upcoming_notifies_according_to_redis(
return True
return False

@sentry_sdk.trace
def save_patch_totals(self, comparison: ComparisonProxy) -> None:
"""Saves patch coverage to the CompareCommit, if it exists.
This is done to make sure the patch coverage reported by notifications and UI is the same
(because they come from the same source)
"""
if comparison.project_coverage_base.commit is None:
# This is the base that will be saved in the CommitComparison
# Even if the patch coverage could come from a different commit
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in other words, we identify a CommitComparison by its project coverage base irrespective of whether its patch coverage base is different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right... but this was a good callout.
The Comparison is created by the UploadFinisher task here. I thought that Pull.compared_to was the project base. But maybe that's not the case... I'll review before merging

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking the code to this (where we decide what is the base_commit for the comparison) it seems that it's always the project base, unless there is no pull, then we use the commit's parent

So I guess the logic is fine. If we have a project base we would have created a comparison. Otherwise there is no Pull object and so there's no Comparison.

But this also means we might emit patch coverage in the UI 🤔
Should we then get_or_create a comparison instead of only updating if it exists... it might be a better idea

head_commit = comparison.head.commit
db_session = head_commit.get_db_session()
patch_coverage = async_to_sync(comparison.get_patch_totals)()
statement = (
CompareCommit.__table__.update()
.where(
and_(
CompareCommit.compare_commit_id == head_commit.id,
CompareCommit.base_commit_id
== comparison.project_coverage_base.commit.id,
)
)
.values(patch_totals=minimal_totals(patch_coverage))
)
db_session.execute(statement)

@sentry_sdk.trace
def submit_third_party_notifications(
self,
current_yaml: UserYaml,
Expand Down Expand Up @@ -562,6 +592,8 @@ def submit_third_party_notifications(
),
)

self.save_patch_totals(comparison)

decoration_type = self.determine_decoration_type_from_pull(
enriched_pull, empty_upload
)
Expand Down Expand Up @@ -687,6 +719,7 @@ def schedule_new_user_activated_task(self, org_ownerid, user_ownerid):
),
)

@sentry_sdk.trace
def fetch_and_update_whether_ci_passed(
self, repository_service, commit, current_yaml
):
Expand Down
Loading
Loading