Skip to content

feat: Add path and query params to test-results endpoint #93264

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

Merged
merged 10 commits into from
Jun 11, 2025
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
66 changes: 66 additions & 0 deletions src/sentry/apidocs/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,3 +1041,69 @@ class PreventParams:
type=str,
description="The name of the repository.",
)
INTERVAL = OpenApiParameter(
name="interval",
location="query",
required=False,
type=str,
description="""The time interval to search for results by.

Available fields are:
- `INTERVAL_30_DAY`
- `INTERVAL_7_DAY`
- `INTERVAL_1_DAY`
""",
)
BRANCH = OpenApiParameter(
name="branch",
location="query",
required=False,
type=str,
description="""The branch to search for results by. If not specified, the default branch is returned.
""",
)
TEST_RESULTS_FILTER_BY = OpenApiParameter(
name="filterBy",
location="query",
required=False,
type=str,
description="""An optional field to filter by, which will constrain the results to only include tests that match the filter.

Available fields are:
- `FLAKY_TESTS`
- `FAILED_TESTS`
- `SLOWEST_TESTS`
- `SKIPPED_TESTS`
""",
)
TEST_RESULTS_SORT_BY = OpenApiParameter(
name="sortBy",
location="query",
required=False,
type=str,
description="""The property to sort results by. If not specified, all results are returned. Use `-`
for descending order.

Available fields are:
- `AVG_DURATION`
- `FLAKE_RATE`
- `FAILURE_RATE`
- `COMMITS_WHERE_FAIL`
- `UPDATED_AT`
""",
)
FIRST = OpenApiParameter(
name="first",
location="query",
required=False,
type=int,
default=20,
description="""The number of results to return from the start of the list.""",
)
LAST = OpenApiParameter(
name="last",
location="query",
required=False,
type=int,
description="""The number of results to return from the end of the list.""",
)
49 changes: 39 additions & 10 deletions src/sentry/codecov/endpoints/TestResults/test_results.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum

from drf_spectacular.utils import extend_schema
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response

Expand Down Expand Up @@ -58,6 +59,12 @@ def has_pagination(self, response):
parameters=[
PreventParams.OWNER,
PreventParams.REPOSITORY,
PreventParams.TEST_RESULTS_SORT_BY,
PreventParams.TEST_RESULTS_FILTER_BY,
PreventParams.INTERVAL,
PreventParams.BRANCH,
PreventParams.FIRST,
PreventParams.LAST,
],
request=None,
responses={
Expand All @@ -70,29 +77,51 @@ def has_pagination(self, response):
def get(self, request: Request, owner: str, repository: str, **kwargs) -> Response:
"""Retrieves the list of test results for a given repository and owner. Also accepts a number of query parameters to filter the results."""

owner = "codecov"
repository = "gazebo"
branch = "main"
sort_by = request.query_params.get("sortBy", OrderingParameter.COMMITS_WHERE_FAIL.value)

if sort_by and sort_by.startswith("-"):
sort_by = sort_by[1:]
direction = OrderingDirection.DESC.value
else:
direction = OrderingDirection.ASC.value

first_param = request.query_params.get("first")
last_param = request.query_params.get("last")

first = int(first_param) if first_param else None
last = int(last_param) if last_param else None

if first and last:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={"details": "Cannot specify both `first` and `last`"},
)

if not first and not last:
first = 20

variables = {
"owner": owner,
"repo": repository,
"filters": {
"branch": branch,
"branch": request.query_params.get("branch", "main"),
Copy link
Contributor

Choose a reason for hiding this comment

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

How would this work w/ master? I suppose main captures more than master these days

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You could just pass in a branch name for master, eventually the frontend will be the SOT, we're just defaulting main here so the user doesn't need to pass a branch for most cases

Copy link
Contributor

Choose a reason for hiding this comment

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

hmm, i would think that there's no fallback for branch unless we're just keeping this here for the time that this will be under development

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's right, just keeping it for development. We can remove as soon as query params are hooked up

"parameter": request.query_params.get("filterBy"),
"interval": (
request.query_params.get("interval", MeasurementInterval.INTERVAL_30_DAY.value)
),
"flags": None,
"interval": MeasurementInterval.INTERVAL_30_DAY.value,
"parameter": None,
"term": None,
"test_suites": None,
},
"ordering": {
"direction": OrderingDirection.DESC.value,
"parameter": OrderingParameter.COMMITS_WHERE_FAIL.value,
"direction": direction,
"parameter": sort_by,
},
"first": 10,
"first": first,
"last": last,
}

client = CodecovApiClient(git_provider_org="codecov")
client = CodecovApiClient(git_provider_org=owner)
graphql_response = client.query(query=query, variables=variables)

test_results = TestResultSerializer().to_representation(graphql_response.json())
Expand Down
151 changes: 146 additions & 5 deletions tests/sentry/codecov/endpoints/test_test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def reverse_url(self, owner="testowner", repository="testrepo"):
)

@patch("sentry.codecov.endpoints.TestResults.test_results.CodecovApiClient")
def test_get_returns_mock_response(self, mock_codecov_client_class):
def test_get_returns_mock_response_with_default_variables(self, mock_codecov_client_class):
mock_graphql_response = {
"data": {
"owner": {
Expand Down Expand Up @@ -83,12 +83,34 @@ def test_get_returns_mock_response(self, mock_codecov_client_class):
url = self.reverse_url()
response = self.client.get(url)

mock_codecov_client_class.assert_called_once_with(git_provider_org="codecov")
assert mock_codecov_client_instance.query.call_count == 1
assert response.status_code == 200
mock_codecov_client_class.assert_called_once_with(git_provider_org="testowner")

assert len(response.data["results"]) == 2
# Verify the correct variables are passed to the GraphQL query
expected_variables = {
"owner": "testowner",
"repo": "testrepo",
"filters": {
"branch": "main",
"parameter": None,
"interval": "INTERVAL_30_DAY",
"flags": None,
"term": None,
"test_suites": None,
},
"ordering": {
"direction": "ASC",
"parameter": "COMMITS_WHERE_FAIL",
},
"first": 20,
"last": None,
}

mock_codecov_client_instance.query.assert_called_once()
call_args = mock_codecov_client_instance.query.call_args
assert call_args[1]["variables"] == expected_variables

assert response.status_code == 200
assert len(response.data["results"]) == 2
assert response.data["pageInfo"]["endCursor"] == "cursor123"
assert response.data["pageInfo"]["hasNextPage"] is False
assert response.data["totalCount"] == 2
Expand All @@ -99,3 +121,122 @@ def test_get_returns_mock_response(self, mock_codecov_client_class):
assert (
response_keys == serializer_fields
), f"Response keys {response_keys} don't match serializer fields {serializer_fields}"

@patch("sentry.codecov.endpoints.TestResults.test_results.CodecovApiClient")
def test_get_with_query_parameters(self, mock_codecov_client_class):
mock_graphql_response = {
"data": {
"owner": {
"repository": {
"__typename": "Repository",
"testAnalytics": {
"testResults": {
"edges": [],
"pageInfo": {"endCursor": None, "hasNextPage": False},
"totalCount": 0,
}
},
}
}
}
}
mock_codecov_client_instance = Mock()
mock_response = Mock()
mock_response.json.return_value = mock_graphql_response
mock_codecov_client_instance.query.return_value = mock_response
mock_codecov_client_class.return_value = mock_codecov_client_instance

url = self.reverse_url()
query_params = {
"branch": "develop",
"filterBy": "FLAKY_TESTS",
"sortBy": "-AVG_DURATION",
"interval": "INTERVAL_7_DAY",
"first": "10",
}
response = self.client.get(url, query_params)

# Verify the correct variables are passed with custom query parameters
expected_variables = {
"owner": "testowner",
"repo": "testrepo",
"filters": {
"branch": "develop",
"parameter": "FLAKY_TESTS",
"interval": "INTERVAL_7_DAY",
"flags": None,
"term": None,
"test_suites": None,
},
"ordering": {
"direction": "DESC",
"parameter": "AVG_DURATION",
},
"first": 10,
"last": None,
}

call_args = mock_codecov_client_instance.query.call_args
assert call_args[1]["variables"] == expected_variables
assert response.status_code == 200

@patch("sentry.codecov.endpoints.TestResults.test_results.CodecovApiClient")
def test_get_with_last_parameter(self, mock_codecov_client_class):
mock_graphql_response = {
"data": {
"owner": {
"repository": {
"__typename": "Repository",
"testAnalytics": {
"testResults": {
"edges": [],
"pageInfo": {"endCursor": None, "hasNextPage": False},
"totalCount": 0,
}
},
}
}
}
}
mock_codecov_client_instance = Mock()
mock_response = Mock()
mock_response.json.return_value = mock_graphql_response
mock_codecov_client_instance.query.return_value = mock_response
mock_codecov_client_class.return_value = mock_codecov_client_instance

url = self.reverse_url()
query_params = {"last": "5"}
response = self.client.get(url, query_params)

# Verify the correct variables are passed with last parameter
expected_variables = {
"owner": "testowner",
"repo": "testrepo",
"filters": {
"branch": "main",
"parameter": None,
"interval": "INTERVAL_30_DAY",
"flags": None,
"term": None,
"test_suites": None,
},
"ordering": {
"direction": "ASC",
"parameter": "COMMITS_WHERE_FAIL",
},
"first": None,
"last": 5,
}

call_args = mock_codecov_client_instance.query.call_args
assert call_args[1]["variables"] == expected_variables
assert response.status_code == 200

def test_get_with_both_first_and_last_returns_bad_request(self):
"""Test that providing both first and last parameters returns a 400 Bad Request error"""
url = self.reverse_url()
query_params = {"first": "10", "last": "5"}
response = self.client.get(url, query_params)

assert response.status_code == 400
assert response.data == {"details": "Cannot specify both `first` and `last`"}
Loading