Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 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
40 changes: 40 additions & 0 deletions api/public/v2/pull/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from api.public.v2.owner.serializers import OwnerSerializer
from api.shared.commit.serializers import CommitTotalsSerializer
from compare.models import CommitComparison
from core.models import Pull, PullStates
from services.comparison import ComparisonReport


class PullSerializer(serializers.ModelSerializer):
Expand All @@ -18,6 +20,7 @@ class PullSerializer(serializers.ModelSerializer):
label="indicates whether the CI process passed for the head commit of this pull"
)
author = OwnerSerializer(label="pull author")
patch = serializers.SerializerMethodField()

class Meta:
model = Pull
Expand All @@ -30,5 +33,42 @@ class Meta:
"state",
"ci_passed",
"author",
"patch",
)
fields = read_only_fields

def get_patch(self, obj: Pull):
# 1) Fetch the CommitComparison for (compared_to, head)
comparison_qs = CommitComparison.objects.filter(
base_commit__commitid=obj.compared_to,
compare_commit__commitid=obj.head,
base_commit__repository_id=obj.repository_id,
compare_commit__repository_id=obj.repository_id,
).select_related("compare_commit", "base_commit")

commit_comparison = comparison_qs.first()
if not commit_comparison or not commit_comparison.is_processed:
return None

# 2) Wrap it in ComparisonReport
cr = ComparisonReport(commit_comparison)

# 3) Summation of patch coverage across impacted files
hits = misses = partials = 0
for f in cr.impacted_files:
pc = f.patch_coverage
if pc:
hits += pc.hits
misses += pc.misses
partials += pc.partials

total_branches = hits + misses + partials
if total_branches == 0:
return None

return dict(
hits=hits,
misses=misses,
partials=partials,
coverage=round(100 * hits / total_branches, 2),
)
90 changes: 80 additions & 10 deletions api/public/v2/tests/test_api_pull_viewset.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import MagicMock, patch

from django.test import override_settings
from django.urls import reverse
Expand All @@ -20,7 +20,8 @@ def setUp(self):
self.org = OwnerFactory()
self.repo = RepositoryFactory(author=self.org)
self.current_owner = OwnerFactory(
permission=[self.repo.repoid], organizations=[self.org.ownerid]
permission=[self.repo.repoid],
organizations=[self.org.ownerid],
)
self.pulls = [
PullFactory(repository=self.repo),
Expand All @@ -29,7 +30,6 @@ def setUp(self):
Pull.objects.filter(pk=self.pulls[1].pk).update(
updatestamp="2023-01-01T00:00:00"
)

self.client = APIClient()
self.client.force_login_owner(self.current_owner)

Expand Down Expand Up @@ -59,6 +59,7 @@ def test_list(self):
"state": "open",
"ci_passed": None,
"author": None,
"patch": None,
},
{
"pullid": self.pulls[0].pullid,
Expand All @@ -69,6 +70,7 @@ def test_list(self):
"state": "open",
"ci_passed": None,
"author": None,
"patch": None,
},
],
"total_pages": 1,
Expand Down Expand Up @@ -100,7 +102,8 @@ def test_list_state(self):
"state": "closed",
"ci_passed": None,
"author": None,
},
"patch": None,
}
],
"total_pages": 1,
}
Expand Down Expand Up @@ -130,7 +133,8 @@ def test_list_start_date(self):
"state": "open",
"ci_passed": None,
"author": None,
},
"patch": None,
}
],
"total_pages": 1,
}
Expand All @@ -157,7 +161,8 @@ def test_list_cursor_pagination(self):
"state": "open",
"ci_passed": None,
"author": None,
},
"patch": None,
}
]
assert data["previous"] is None
assert data["next"] is not None
Expand All @@ -174,15 +179,15 @@ def test_list_cursor_pagination(self):
"state": "open",
"ci_passed": None,
"author": None,
},
"patch": None,
}
]
assert data["previous"] is not None
assert data["next"] is None

@patch("api.shared.repo.repository_accessors.RepoAccessors.get_repo_permissions")
def test_retrieve(self, get_repo_permissions):
get_repo_permissions.return_value = (True, True)

res = self.client.get(
reverse(
"api-v2-pulls-detail",
Expand All @@ -204,6 +209,7 @@ def test_retrieve(self, get_repo_permissions):
"state": "open",
"ci_passed": None,
"author": None,
"patch": None,
}

@patch("api.shared.permissions.RepositoryArtifactPermissions.has_permission")
Expand All @@ -215,7 +221,6 @@ def test_no_pull_if_unauthenticated_token_request(
):
repository_artifact_permissions_has_permission.return_value = False
super_token_permissions_has_permission.return_value = False

res = self.client.get(
reverse(
"api-v2-pulls-detail",
Expand All @@ -238,7 +243,6 @@ def test_no_pull_if_not_super_token_nor_user_token(
self, repository_artifact_permissions_has_permission
):
repository_artifact_permissions_has_permission.return_value = False

res = self.client.get(
reverse(
"api-v2-pulls-detail",
Expand Down Expand Up @@ -301,4 +305,70 @@ def test_pull_with_valid_super_token(self):
"state": "open",
"ci_passed": None,
"author": None,
"patch": None,
}

@patch("api.public.v2.pull.serializers.ComparisonReport")
@patch("api.public.v2.pull.serializers.CommitComparison.objects.filter")
def test_retrieve_with_patch_coverage(self, mock_cc_filter, mock_comparison_report):
mock_cc_instance = MagicMock(is_processed=True)
mock_cc_filter.return_value.select_related.return_value.first.return_value = (
mock_cc_instance
)

mock_file = MagicMock()
mock_file.patch_coverage.hits = 10
mock_file.patch_coverage.misses = 5
mock_file.patch_coverage.partials = 2
mock_comparison_report.return_value.impacted_files = [mock_file]

res = self.client.get(
reverse(
"api-v2-pulls-detail",
kwargs={
"service": self.org.service,
"owner_username": self.org.username,
"repo_name": self.repo.name,
"pullid": self.pulls[0].pullid,
},
)
)
assert res.status_code == 200
data = res.json()
assert data["patch"] == {
"hits": 10,
"misses": 5,
"partials": 2,
"coverage": 58.82,
}

@patch("api.public.v2.pull.serializers.ComparisonReport")
@patch("api.public.v2.pull.serializers.CommitComparison.objects.filter")
def test_retrieve_with_patch_coverag_no_branches(
self, mock_cc_filter, mock_comparison_report
):
mock_cc_instance = MagicMock(is_processed=True)
mock_cc_filter.return_value.select_related.return_value.first.return_value = (
mock_cc_instance
)

mock_file = MagicMock()
mock_file.patch_coverage.hits = 0
mock_file.patch_coverage.misses = 0
mock_file.patch_coverage.partials = 0
mock_comparison_report.return_value.impacted_files = [mock_file]

res = self.client.get(
reverse(
"api-v2-pulls-detail",
kwargs={
"service": self.org.service,
"owner_username": self.org.username,
"repo_name": self.repo.name,
"pullid": self.pulls[0].pullid,
},
)
)
assert res.status_code == 200
data = res.json()
assert data["patch"] is None
Loading