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: don't require BASE report for patch in comparison #650

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion services/comparison/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from dataclasses import dataclass
from typing import Dict, List, Optional

import sentry_sdk
from shared.reports.changes import get_changes_using_rust, run_comparison_using_rust
from shared.reports.types import Change
from shared.reports.types import Change, ReportTotals
from shared.torngit.exceptions import (
TorngitClientGeneralError,
)
Expand Down Expand Up @@ -202,6 +203,16 @@ async def get_changes(self) -> Optional[List[Change]]:
)
return self._changes

@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.
"""
diff = await self.get_diff(use_original_base=True)
totals = self.head.report.apply_diff(diff)
return totals

async def get_behind_by(self):
async with self._behind_by_lock:
if self._behind_by is None:
Expand Down
107 changes: 82 additions & 25 deletions tasks/compute_comparison.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from typing import Literal, TypedDict

import sentry_sdk
from asgiref.sync import async_to_sync
from shared.celery_config import compute_comparison_task_name
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

Expand All @@ -13,7 +16,6 @@
from database.models import CompareCommit, CompareComponent, CompareFlag
from database.models.reports import ReportLevelTotals, RepositoryFlag
from helpers.github_installation import get_installation_name_for_owner_for_task
from helpers.metrics import metrics
from services.archive import ArchiveService
from services.comparison import ComparisonContext, ComparisonProxy, FilteredComparison
from services.comparison.types import Comparison, FullCommit
Expand All @@ -24,51 +26,89 @@
log = logging.getLogger(__name__)


ComputeComparisonTaskErrors = (
Literal["missing_head_report"]
| Literal["missing_base_report"]
| Literal["torngit_rate_limit"]
)


class ComputeComparisonTaskReturn(TypedDict):
success: bool
error: ComputeComparisonTaskErrors | None


class ComputeComparisonTask(BaseCodecovTask, name=compute_comparison_task_name):
def run_impl(self, db_session, comparison_id, *args, **kwargs):
comparison = db_session.query(CompareCommit).get(comparison_id)
def run_impl(
self, db_session, comparison_id, *args, **kwargs
) -> ComputeComparisonTaskReturn:
comparison: CompareCommit = db_session.query(CompareCommit).get(comparison_id)
repo = comparison.compare_commit.repository
log_extra = dict(comparison_id=comparison_id, repoid=repo.repoid)
log_extra = dict(
comparison_id=comparison_id,
repoid=repo.repoid,
commit=comparison.compare_commit.commitid,
)
log.info("Computing comparison", extra=log_extra)
current_yaml = self.get_yaml_commit(comparison.compare_commit)
current_yaml = get_repo_yaml(repo)
installation_name_to_use = get_installation_name_for_owner_for_task(
db_session, self.name, repo.owner
)

with metrics.timer(f"{self.metrics_prefix}.get_comparison_proxy"):
comparison_proxy = self.get_comparison_proxy(
comparison, current_yaml, installation_name_to_use
)
comparison_proxy = self.get_comparison_proxy(
comparison, current_yaml, installation_name_to_use
)
if not comparison_proxy.has_head_report():
comparison.error = CompareCommitError.missing_head_report.value
comparison.state = CompareCommitState.error.value
log.warning("Comparison doesn't have HEAD report", extra=log_extra)
return {"successful": False, "error": "missing_head_report"}

# 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 not comparison_proxy.has_project_coverage_base_report():
comparison.error = CompareCommitError.missing_base_report.value
elif not comparison_proxy.has_head_report():
comparison.error = CompareCommitError.missing_head_report.value
log.warning(
"Comparison doesn't have BASE report",
extra={"base_commit": comparison.base_commit.commitid, **log_extra},
)
comparison.state = CompareCommitState.error.value
return {"successful": False, "error": "missing_base_report"}
else:
comparison.error = None

if comparison.error:
comparison.state = CompareCommitState.error.value
log.warn("Compute comparison failed, %s", comparison.error, extra=log_extra)
return {"successful": False}
try:
with metrics.timer(f"{self.metrics_prefix}.serialize_impacted_files") as tm:
impacted_files = self.serialize_impacted_files(comparison_proxy)
impacted_files = self.serialize_impacted_files(comparison_proxy)
except TorngitRateLimitError:
log.warning(
"Unable to compute comparison due to rate limit error",
extra=dict(
comparison_id=comparison_id, repoid=comparison.compare_commit.repoid
),
)
return {"successful": False}
log.info("Files impact calculated", extra=dict(timing_ms=tm.ms, **log_extra))
with metrics.timer(f"{self.metrics_prefix}.store_results"):
path = self.store_results(comparison, impacted_files)
comparison.state = CompareCommitState.error.value
return {"successful": False, "error": "torngit_rate_limit"}

log.info("Files impact calculated", extra=log_extra)
path = self.store_results(comparison, impacted_files)

comparison.report_storage_path = path
comparison.patch_totals = impacted_files.get("changes_summary").get(
calculated_patch_totals = impacted_files.get("changes_summary").get(
"patch_totals"
)
if calculated_patch_totals != comparison.patch_totals:
log.warning(

Check warning on line 103 in tasks/compute_comparison.py

View check run for this annotation

Codecov Notifications / codecov/patch

tasks/compute_comparison.py#L103

Added line #L103 was not covered by tests
Comment on lines +99 to +103
Copy link
Contributor

Choose a reason for hiding this comment

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

under which circumstances would this ever happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know, to be honest. Hopefully never.

But as I'm switching from that calculation to a copy of the one we use for the notifications I thought it would be prudent to add this just in case.

I also thought it was a good idea to always keep the result that come from the same place

"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)
db_session.commit()
Expand All @@ -79,6 +119,21 @@
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,
"coverage": totals.coverage,
}

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 All @@ -93,6 +148,7 @@
comparison_proxy,
)

@sentry_sdk.trace
def create_or_update_flag_comparisons(
self,
db_session,
Expand Down Expand Up @@ -193,6 +249,7 @@
db_session.add(flag_comparison)
db_session.flush()

@sentry_sdk.trace
def compute_component_comparisons(
self, db_session, comparison: CompareCommit, comparison_proxy: ComparisonProxy
):
Expand Down Expand Up @@ -255,9 +312,7 @@
db_session.add(component_comparison)
db_session.flush()

def get_yaml_commit(self, commit):
return get_repo_yaml(commit.repository)

@sentry_sdk.trace
def get_comparison_proxy(
self, comparison, current_yaml, installation_name_to_use: str | None = None
):
Expand Down Expand Up @@ -289,9 +344,11 @@
),
)

@sentry_sdk.trace
def serialize_impacted_files(self, comparison_proxy):
return async_to_sync(comparison_proxy.get_impacted_files)()

@sentry_sdk.trace
def store_results(self, comparison, impacted_files):
repository = comparison.compare_commit.repository
storage_service = ArchiveService(repository)
Expand Down
52 changes: 45 additions & 7 deletions tasks/tests/unit/test_compute_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from shared.reports.readonly import ReadOnlyReport
from shared.reports.resources import Report
from shared.reports.types import ReportTotals
from shared.torngit.exceptions import TorngitRateLimitError
from shared.yaml import UserYaml

Expand Down Expand Up @@ -311,20 +312,44 @@ def test_update_existing_flag_comparisons(
assert compare_flag_records[0].repositoryflag_id == repositoryflag.id_
assert compare_flag_records[0].patch_totals is not None

def test_set_state_to_error_missing_base_report(self, dbsession, mocker):
def test_set_state_to_error_missing_base_report(
self, dbsession, mocker, sample_report
):
comparison = CompareCommitFactory.create()
# We need a head report, but no base report
head_commit = comparison.compare_commit
mocker.patch.object(
ReportService,
"get_existing_report_for_commit",
side_effect=lambda commit,
*args,
**kwargs: ReadOnlyReport.create_from_report(sample_report)
if commit == head_commit
else None,
)
patch_totals = ReportTotals(
files=3, lines=200, hits=100, misses=100, coverage="50%"
)
mocker.patch(
"tasks.compute_comparison.ComparisonProxy.get_patch_totals",
return_value=patch_totals,
)
dbsession.add(comparison)
dbsession.flush()
task = ComputeComparisonTask()
mocker.patch.object(
ReadOnlyReport, "should_load_rust_version", return_value=True
)
mocker.patch.object(
ReportService, "get_existing_report_for_commit", return_value=None
)
task.run_impl(dbsession, comparison.id)
result = task.run_impl(dbsession, comparison.id)
dbsession.flush()
assert result == {"successful": False, "error": "missing_base_report"}
assert comparison.state == CompareCommitState.error.value
assert comparison.patch_totals == {
"hits": 100,
"misses": 100,
"partials": 0,
"coverage": "50%",
}
assert comparison.error == CompareCommitError.missing_base_report.value

def test_set_state_to_error_missing_head_report(
Expand All @@ -351,6 +376,13 @@ def test_run_task_ratelimit_error(self, dbsession, mocker, sample_report):
comparison = CompareCommitFactory.create()
dbsession.add(comparison)
dbsession.flush()
patch_totals = ReportTotals(
files=3, lines=200, hits=100, misses=100, coverage="50%"
)
mocker.patch(
"tasks.compute_comparison.ComparisonProxy.get_patch_totals",
return_value=patch_totals,
)
mocker.patch.object(
ComputeComparisonTask,
"serialize_impacted_files",
Expand All @@ -363,9 +395,15 @@ def test_run_task_ratelimit_error(self, dbsession, mocker, sample_report):
return_value=ReadOnlyReport.create_from_report(sample_report),
)
res = task.run_impl(dbsession, comparison.id)
assert res == {"successful": False}
assert res == {"successful": False, "error": "torngit_rate_limit"}
dbsession.flush()
assert comparison.state == CompareCommitState.pending.value
assert comparison.state == CompareCommitState.error.value
assert comparison.patch_totals == {
"hits": 100,
"misses": 100,
"partials": 0,
"coverage": "50%",
}
assert comparison.error is None

def test_compute_component_comparisons(
Expand Down
Loading