From f6b2bb02beb69a705d171b3d1a999b8bae659795 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 10 Oct 2024 14:20:59 -0400 Subject: [PATCH 1/3] feat: add total_{fail, pass, skip}_count field to TestResult GQL model --- graphql_api/tests/test_test_result.py | 93 +++++++++++++++++++ .../types/test_results/test_results.graphql | 3 + .../types/test_results/test_results.py | 18 ++++ utils/test_results.py | 2 + 4 files changed, 116 insertions(+) diff --git a/graphql_api/tests/test_test_result.py b/graphql_api/tests/test_test_result.py index bcef2b35ab..8542062359 100644 --- a/graphql_api/tests/test_test_result.py +++ b/graphql_api/tests/test_test_result.py @@ -227,3 +227,96 @@ def test_fetch_test_result_avg_duration(self) -> None: assert result["owner"]["repository"]["testAnalytics"]["testResults"]["edges"][ 0 ]["node"]["avgDuration"] == (5.6 / 3) + + def test_fetch_test_result_total_fail_count(self) -> None: + query = """ + query { + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + testAnalytics { + testResults { + edges { + node { + totalFailCount + } + } + } + } + } + } + } + } + """ % (self.owner.username, self.repository.name) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert ( + result["owner"]["repository"]["testAnalytics"]["testResults"]["edges"][0][ + "node" + ]["totalFailCount"] + == 9 + ) + + def test_fetch_test_result_total_skip_count(self) -> None: + query = """ + query { + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + testAnalytics { + testResults { + edges { + node { + totalSkipCount + } + } + } + } + } + } + } + } + """ % (self.owner.username, self.repository.name) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert ( + result["owner"]["repository"]["testAnalytics"]["testResults"]["edges"][0][ + "node" + ]["totalSkipCount"] + == 6 + ) + + def test_fetch_test_result_total_pass_count(self) -> None: + query = """ + query { + owner(username: "%s") { + repository(name: "%s") { + ... on Repository { + testAnalytics { + testResults { + edges { + node { + totalPassCount + } + } + } + } + } + } + } + } + """ % (self.owner.username, self.repository.name) + + result = self.gql_request(query, owner=self.owner) + + assert "errors" not in result + assert ( + result["owner"]["repository"]["testAnalytics"]["testResults"]["edges"][0][ + "node" + ]["totalPassCount"] + == 3 + ) diff --git a/graphql_api/types/test_results/test_results.graphql b/graphql_api/types/test_results/test_results.graphql index ad302cfbbc..ca5ac69e2e 100644 --- a/graphql_api/types/test_results/test_results.graphql +++ b/graphql_api/types/test_results/test_results.graphql @@ -6,4 +6,7 @@ type TestResult { flakeRate: Float! avgDuration: Float! lastDuration: Float! + totalFailCount: Int! + totalSkipCount: Int! + totalPassCount: Int! } diff --git a/graphql_api/types/test_results/test_results.py b/graphql_api/types/test_results/test_results.py index 6795c3c82d..005fab4447 100644 --- a/graphql_api/types/test_results/test_results.py +++ b/graphql_api/types/test_results/test_results.py @@ -13,6 +13,9 @@ class TestDict(TypedDict): avg_duration: float last_duration: float flake_rate: float + total_fail_count: int + total_skip_count: int + total_pass_count: int test_result_bindable = ObjectType("TestResult") @@ -51,3 +54,18 @@ def resolve_avg_duration(test: TestDict, _: GraphQLResolveInfo) -> float: @test_result_bindable.field("lastDuration") def resolve_last_duration(test: TestDict, _: GraphQLResolveInfo) -> float: return test["last_duration"] + + +@test_result_bindable.field("totalFailCount") +def resolve_total_fail_count(test: TestDict, _: GraphQLResolveInfo) -> int: + return test["total_fail_count"] + + +@test_result_bindable.field("totalSkipCount") +def resolve_total_skip_count(test: TestDict, _: GraphQLResolveInfo) -> int: + return test["total_skip_count"] + + +@test_result_bindable.field("totalPassCount") +def resolve_total_pass_count(test: TestDict, _: GraphQLResolveInfo) -> int: + return test["total_pass_count"] diff --git a/utils/test_results.py b/utils/test_results.py index 0af961c0b2..1672a72e81 100644 --- a/utils/test_results.py +++ b/utils/test_results.py @@ -172,6 +172,8 @@ def generate_test_results( total_flaky_fail_count=Cast( Sum(F("flaky_fail_count")), output_field=FloatField() ), + total_skip_count=Cast(Sum(F("skip_count")), output_field=FloatField()), + total_pass_count=Cast(Sum(F("pass_count")), output_field=FloatField()), failure_rate=Case( When( total_test_count=0, From 18a237a848057e34873b1ffc424267fa633a7caa Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 10 Oct 2024 14:45:51 -0400 Subject: [PATCH 2/3] feat: add total slow tests field to testResultsAggregates --- graphql_api/tests/test_test_analytics.py | 20 +++++++++++-------- .../test_results_aggregates.graphql | 2 ++ utils/test_results.py | 9 ++++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/graphql_api/tests/test_test_analytics.py b/graphql_api/tests/test_test_analytics.py index 0b816901f2..9a5eb91f55 100644 --- a/graphql_api/tests/test_test_analytics.py +++ b/graphql_api/tests/test_test_analytics.py @@ -662,17 +662,19 @@ def test_test_results_aggregates(self) -> None: ) res = self.fetch_test_analytics( repo.name, - """testResultsAggregates { totalDuration, slowestTestsDuration, totalFails, totalSkips, totalDurationPercentChange, slowestTestsDurationPercentChange, totalFailsPercentChange, totalSkipsPercentChange }""", + """testResultsAggregates { totalDuration, slowestTestsDuration, totalFails, totalSkips, totalSlowTests, totalDurationPercentChange, slowestTestsDurationPercentChange, totalFailsPercentChange, totalSkipsPercentChange, totalSlowTestsPercentChange }""", ) assert res["testResultsAggregates"] == { "totalDuration": 570.0, - "slowestTestsDuration": 29.0, - "totalFails": 10, - "totalSkips": 5, "totalDurationPercentChange": -63.1068, + "slowestTestsDuration": 29.0, "slowestTestsDurationPercentChange": -50.84746, + "totalFails": 10, "totalFailsPercentChange": 100.0, + "totalSkips": 5, "totalSkipsPercentChange": -50.0, + "totalSlowTests": 1, + "totalSlowTestsPercentChange": 0.0, } def test_test_results_aggregates_no_history(self) -> None: @@ -696,18 +698,20 @@ def test_test_results_aggregates_no_history(self) -> None: res = self.fetch_test_analytics( repo.name, - """testResultsAggregates { totalDuration, slowestTestsDuration, totalFails, totalSkips, totalDurationPercentChange, slowestTestsDurationPercentChange, totalFailsPercentChange, totalSkipsPercentChange }""", + """testResultsAggregates { totalDuration, slowestTestsDuration, totalFails, totalSkips, totalSlowTests, totalDurationPercentChange, slowestTestsDurationPercentChange, totalFailsPercentChange, totalSkipsPercentChange, totalSlowTestsPercentChange }""", ) assert res["testResultsAggregates"] == { "totalDuration": 570.0, - "slowestTestsDuration": 29.0, - "totalFails": 10, - "totalSkips": 5, "totalDurationPercentChange": None, + "slowestTestsDuration": 29.0, "slowestTestsDurationPercentChange": None, + "totalFails": 10, "totalFailsPercentChange": None, + "totalSkips": 5, "totalSkipsPercentChange": None, + "totalSlowTests": 1, + "totalSlowTestsPercentChange": None, } def test_flake_aggregates(self) -> None: diff --git a/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql b/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql index f588587d0c..e225bc1ff3 100644 --- a/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql +++ b/graphql_api/types/test_results_aggregates/test_results_aggregates.graphql @@ -7,4 +7,6 @@ type TestResultsAggregates { totalFailsPercentChange: Float totalSkips: Int! totalSkipsPercentChange: Float + totalSlowTests: Int! + totalSlowTestsPercentChange: Float } diff --git a/utils/test_results.py b/utils/test_results.py index 1672a72e81..31ecf91468 100644 --- a/utils/test_results.py +++ b/utils/test_results.py @@ -264,6 +264,7 @@ def get_test_results_aggregate_numbers( slowest_tests_duration=slowest_tests_duration, skips=Sum("skip_count"), fails=Sum("fail_count"), + total_slow_tests=Value(slow_test_threshold(num_tests)), ) return test_headers[0] if len(test_headers) > 0 else {} @@ -282,7 +283,13 @@ def generate_test_results_aggregates( past_numbers = get_test_results_aggregate_numbers(repo, double_time_ago, since) return curr_numbers | get_percent_change( - ["total_duration", "slowest_tests_duration", "skips", "fails"], + [ + "total_duration", + "slowest_tests_duration", + "skips", + "fails", + "total_slow_tests", + ], curr_numbers, past_numbers, ) From 9101e3deac2f3bac2a3755a2ab7dadf4bc2a6cc6 Mon Sep 17 00:00:00 2001 From: joseph-sentry Date: Thu, 10 Oct 2024 14:49:04 -0400 Subject: [PATCH 3/3] feat: add filtering by search term for test results --- graphql_api/tests/test_test_analytics.py | 39 +++++++++++++++++++ .../types/inputs/test_results_filters.graphql | 1 + .../types/test_analytics/test_analytics.py | 1 + utils/test_results.py | 4 ++ 4 files changed, 45 insertions(+) diff --git a/graphql_api/tests/test_test_analytics.py b/graphql_api/tests/test_test_analytics.py index 9a5eb91f55..296c97abcf 100644 --- a/graphql_api/tests/test_test_analytics.py +++ b/graphql_api/tests/test_test_analytics.py @@ -588,6 +588,45 @@ def test_flake_rate_filtering_on_test_results(self) -> None: ] } + def test_flake_rate_filtering_by_term(self) -> None: + repo = RepositoryFactory(author=self.owner, active=True, private=True) + test = TestFactory(repository=repo, name="hello") + _ = DailyTestRollupFactory( + test=test, + date=datetime.date.today() - datetime.timedelta(days=1), + repoid=repo.repoid, + pass_count=1, + fail_count=1, + flaky_fail_count=1, + ) + _ = DailyTestRollupFactory( + test=test, + date=datetime.date.today(), + repoid=repo.repoid, + pass_count=3, + fail_count=0, + flaky_fail_count=0, + ) + test_2 = TestFactory(repository=repo, name="world") + _ = DailyTestRollupFactory( + test=test_2, + date=datetime.date.today(), + repoid=repo.repoid, + pass_count=2, + fail_count=3, + flaky_fail_count=1, + ) + res = self.fetch_test_analytics( + repo.name, + """testResults(filters: { term: "hello" }) { edges { node { name failureRate } } }""", + ) + + assert res["testResults"] == { + "edges": [ + {"node": {"name": test.name, "failureRate": 0.2}}, + ] + } + def test_desc_flake_rate_ordering_on_test_results(self) -> None: repo = RepositoryFactory(author=self.owner, active=True, private=True) test = TestFactory(repository=repo) diff --git a/graphql_api/types/inputs/test_results_filters.graphql b/graphql_api/types/inputs/test_results_filters.graphql index 360f4fc4d6..6a38a89280 100644 --- a/graphql_api/types/inputs/test_results_filters.graphql +++ b/graphql_api/types/inputs/test_results_filters.graphql @@ -4,6 +4,7 @@ input TestResultsFilters { test_suites: [String!] flags: [String!] history: MeasurementInterval + term: String } input TestResultsOrdering { diff --git a/graphql_api/types/test_analytics/test_analytics.py b/graphql_api/types/test_analytics/test_analytics.py index 908a25632b..5515c702d3 100644 --- a/graphql_api/types/test_analytics/test_analytics.py +++ b/graphql_api/types/test_analytics/test_analytics.py @@ -48,6 +48,7 @@ async def resolve_test_results( parameter=parameter, testsuites=filters.get("test_suites") if filters else None, flags=filters.get("flags") if filters else None, + term=filters.get("term") if filters else None, ) return await queryset_to_connection( diff --git a/utils/test_results.py b/utils/test_results.py index 31ecf91468..464e20a4da 100644 --- a/utils/test_results.py +++ b/utils/test_results.py @@ -66,6 +66,7 @@ def generate_test_results( parameter: GENERATE_TEST_RESULT_PARAM | None = None, testsuites: list[str] | None = None, flags: list[str] | None = None, + term: str | None = None, ) -> QuerySet: """ Function that retrieves aggregated information about all tests in a given repository, for a given time range, optionally filtered by branch name. @@ -103,6 +104,9 @@ def generate_test_results( totals = totals.filter(test_id__in=test_ids) + if term is not None: + totals = totals.filter(test__name__icontains=term) + match parameter: case GENERATE_TEST_RESULT_PARAM.FLAKY: flakes = Flake.objects.filter(