diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_flag_filter__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_flag_filter__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_flag_filter__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_term_filter__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_term_filter__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_term_filter__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_testsuite_filter__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_testsuite_filter__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__analytics_testsuite_filter__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results__0.json new file mode 100644 index 0000000000..060e8b18f7 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results__0.json @@ -0,0 +1,102 @@ +[ + { + "name": "test5", + "test_id": "test_id5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test4", + "test_id": "test_id4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test3", + "test_id": "test_id3", + "testsuite": [ + "testsuite3" + ], + "flags": [ + "flag3" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-03T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test2", + "test_id": "test_id2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_asc__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_asc__0.json new file mode 100644 index 0000000000..03a0a09e77 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_asc__0.json @@ -0,0 +1,102 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test2", + "test_id": "test_id2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test3", + "test_id": "test_id3", + "testsuite": [ + "testsuite3" + ], + "flags": [ + "flag3" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-03T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test4", + "test_id": "test_id4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + }, + { + "name": "test5", + "test_id": "test_id5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after__0.json new file mode 100644 index 0000000000..320eb11525 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test2", + "test_id": "test_id2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after_no_next__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after_no_next__0.json new file mode 100644 index 0000000000..d036dd4833 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_first_1_after_no_next__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test5", + "test_id": "test_id5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1__0.json new file mode 100644 index 0000000000..d036dd4833 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test5", + "test_id": "test_id5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before__0.json new file mode 100644 index 0000000000..453531007c --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test4", + "test_id": "test_id4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before_no_previous__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before_no_previous__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_asc_last_1_before_no_previous__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1__0.json new file mode 100644 index 0000000000..d036dd4833 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test5", + "test_id": "test_id5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after__0.json new file mode 100644 index 0000000000..453531007c --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test4", + "test_id": "test_id4", + "testsuite": [ + "testsuite4" + ], + "flags": [ + "flag4" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-04T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after_no_next__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after_no_next__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_first_1_after_no_next__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1__0.json new file mode 100644 index 0000000000..405d02c5ba --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test1", + "test_id": "test_id1", + "testsuite": [ + "testsuite1" + ], + "flags": [ + "flag1" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-01T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 1, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before__0.json new file mode 100644 index 0000000000..320eb11525 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test2", + "test_id": "test_id2", + "testsuite": [ + "testsuite2" + ], + "flags": [ + "flag2" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-02T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before_no_previous__0.json b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before_no_previous__0.json new file mode 100644 index 0000000000..d036dd4833 --- /dev/null +++ b/graphql_api/tests/snapshots/analytics__TestAnalyticsTestCase__results_pagination_last_1_before_no_previous__0.json @@ -0,0 +1,22 @@ +[ + { + "name": "test5", + "test_id": "test_id5", + "testsuite": [ + "testsuite5" + ], + "flags": [ + "flag5" + ], + "failure_rate": 0.1, + "flake_rate": 0.0, + "updated_at": "2024-01-05T00:00:00", + "avg_duration": 100.0, + "total_fail_count": 1, + "total_flaky_fail_count": 0, + "total_pass_count": 1, + "total_skip_count": 1, + "commits_where_fail": 1, + "last_duration": 100.0 + } +] diff --git a/graphql_api/tests/test_test_analytics.py b/graphql_api/tests/test_test_analytics.py index 777d8f41d0..7588dce071 100644 --- a/graphql_api/tests/test_test_analytics.py +++ b/graphql_api/tests/test_test_analytics.py @@ -15,7 +15,6 @@ ) from graphql_api.types.enums.enum_types import MeasurementInterval from graphql_api.types.test_analytics.test_analytics import ( - TestResultConnection, TestResultsRow, encode_cursor, generate_test_results, @@ -75,6 +74,53 @@ def mock_storage(mocker): rows = [RowFactory()(datetime.datetime(2024, 1, 1 + i)) for i in range(5)] +rows_with_duplicate_names = [ + RowFactory()(datetime.datetime(2024, 1, 1 + i)) for i in range(5) +] +for i in range(0, len(rows_with_duplicate_names) - 1, 2): + rows_with_duplicate_names[i]["name"] = rows_with_duplicate_names[i + 1]["name"] + + +def dedup(rows: list[dict]) -> list[dict]: + by_name = {} + for row in rows: + if row["name"] not in by_name: + by_name[row["name"]] = [] + by_name[row["name"]].append(row) + + result = [] + for name, group in by_name.items(): + if len(group) == 1: + result.append(group[0]) + continue + + weights = [r["total_pass_count"] + r["total_fail_count"] for r in group] + total_weight = sum(weights) + + merged = { + "name": name, + "testsuite": sorted({r["testsuite"] for r in group}), + "flags": sorted({flag for r in group for flag in r["flags"]}), + "test_id": group[0]["test_id"], # Keep first test_id + "failure_rate": sum(r["failure_rate"] * w for r, w in zip(group, weights)) + / total_weight, + "flake_rate": sum(r["flake_rate"] * w for r, w in zip(group, weights)) + / total_weight, + "updated_at": max(r["updated_at"] for r in group), + "avg_duration": sum(r["avg_duration"] * w for r, w in zip(group, weights)) + / total_weight, + "total_fail_count": sum(r["total_fail_count"] for r in group), + "total_flaky_fail_count": sum(r["total_flaky_fail_count"] for r in group), + "total_pass_count": sum(r["total_pass_count"] for r in group), + "total_skip_count": sum(r["total_skip_count"] for r in group), + "commits_where_fail": sum(r["commits_where_fail"] for r in group), + "last_duration": max(r["last_duration"] for r in group), + } + result.append(merged) + + return sorted(result, key=lambda x: x["updated_at"], reverse=True) + + def row_to_camel_case(row: dict) -> dict: return { "commitsFailed" @@ -89,6 +135,7 @@ def row_to_camel_case(row: dict) -> dict: test_results_table = pl.DataFrame(rows) +test_results_table_with_duplicate_names = pl.DataFrame(rows_with_duplicate_names) def base64_encode_string(x: str) -> str: @@ -143,6 +190,21 @@ def store_in_storage(repository, mock_storage): ) +@pytest.fixture +def store_in_redis_with_duplicate_names(repository): + redis = get_redis_connection() + redis.set( + f"test_results:{repository.repoid}:{repository.branch}:30", + test_results_table_with_duplicate_names.write_ipc(None).getvalue(), + ) + + yield + + redis.delete( + f"test_results:{repository.repoid}:{repository.branch}:30", + ) + + class TestAnalyticsTestCase( GraphQLTestHelper, ): @@ -174,7 +236,7 @@ def test_get_test_results_no_redis( m.assert_called_once_with(repository.repoid, repository.branch) def test_test_results( - self, transactional_db, repository, store_in_redis, mock_storage + self, transactional_db, repository, store_in_redis, mock_storage, snapshot ): test_results = generate_test_results( repoid=repository.repoid, @@ -183,25 +245,21 @@ def test_test_results( measurement_interval=MeasurementInterval.INTERVAL_30_DAY, ) assert test_results is not None - assert test_results == TestResultConnection( - total_count=5, - edges=[ - { - "cursor": cursor(row), - "node": TestResultsRow(**row), - } - for row in reversed(rows) - ], - page_info={ - "has_next_page": False, - "has_previous_page": False, - "start_cursor": cursor(rows[4]), - "end_cursor": cursor(rows[0]), - }, - ) + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[4]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] def test_test_results_asc( - self, transactional_db, repository, store_in_redis, mock_storage + self, transactional_db, repository, store_in_redis, mock_storage, snapshot ): test_results = generate_test_results( repoid=repository.repoid, @@ -210,22 +268,18 @@ def test_test_results_asc( measurement_interval=MeasurementInterval.INTERVAL_30_DAY, ) assert test_results is not None - assert test_results == TestResultConnection( - total_count=5, - edges=[ - { - "cursor": cursor(row), - "node": TestResultsRow(**row), - } - for row in rows - ], - page_info={ - "has_next_page": False, - "has_previous_page": False, - "start_cursor": cursor(rows[0]), - "end_cursor": cursor(rows[4]), - }, - ) + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[4]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] @pytest.mark.parametrize( "first, after, last, before, has_next_page, has_previous_page, start_cursor, end_cursor, expected_rows", @@ -318,6 +372,7 @@ def test_test_results_pagination( repository, store_in_redis, mock_storage, + snapshot, ): test_results = generate_test_results( repoid=repository.repoid, @@ -329,22 +384,18 @@ def test_test_results_pagination( before=before, last=last, ) - assert test_results == TestResultConnection( - total_count=5, - edges=[ - { - "cursor": cursor(row), - "node": TestResultsRow(**row), - } - for row in expected_rows - ], - page_info={ - "has_next_page": has_next_page, - "has_previous_page": has_previous_page, - "start_cursor": start_cursor, - "end_cursor": end_cursor, - }, - ) + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": has_next_page, + "has_previous_page": has_previous_page, + "start_cursor": start_cursor, + "end_cursor": end_cursor, + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] @pytest.mark.parametrize( "first, after, last, before, has_next_page, has_previous_page, start_cursor, end_cursor, expected_rows", @@ -437,6 +488,7 @@ def test_test_results_pagination_asc( repository, store_in_redis, mock_storage, + snapshot, ): test_results = generate_test_results( repoid=repository.repoid, @@ -448,24 +500,22 @@ def test_test_results_pagination_asc( before=before, last=last, ) - assert test_results == TestResultConnection( - total_count=5, - edges=[ - { - "cursor": cursor(row), - "node": TestResultsRow(**row), - } - for row in expected_rows - ], - page_info={ - "has_next_page": has_next_page, - "has_previous_page": has_previous_page, - "start_cursor": start_cursor, - "end_cursor": end_cursor, - }, - ) + assert test_results.total_count == 5 + assert test_results.page_info == { + "has_next_page": has_next_page, + "has_previous_page": has_previous_page, + "start_cursor": start_cursor, + "end_cursor": end_cursor, + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] - def test_test_analytics_term_filter(self, repository, store_in_redis, mock_storage): + def test_test_analytics_term_filter( + self, repository, store_in_redis, mock_storage, snapshot + ): test_results = generate_test_results( repoid=repository.repoid, term=rows[0]["name"][2:], @@ -474,23 +524,22 @@ def test_test_analytics_term_filter(self, repository, store_in_redis, mock_stora measurement_interval=MeasurementInterval.INTERVAL_30_DAY, ) assert test_results is not None - assert test_results == TestResultConnection( - total_count=1, - edges=[ - { - "cursor": cursor(rows[0]), - "node": TestResultsRow(**rows[0]), - }, - ], - page_info={ - "has_next_page": False, - "has_previous_page": False, - "start_cursor": cursor(rows[0]), - "end_cursor": cursor(rows[0]), - }, - ) + assert test_results.total_count == 1 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] - def test_test_analytics_testsuite_filter(self, repository, store_in_redis): + def test_test_analytics_testsuite_filter( + self, repository, store_in_redis, snapshot + ): test_results = generate_test_results( repoid=repository.repoid, testsuites=[rows[0]["testsuite"]], @@ -499,23 +548,22 @@ def test_test_analytics_testsuite_filter(self, repository, store_in_redis): measurement_interval=MeasurementInterval.INTERVAL_30_DAY, ) assert test_results is not None - assert test_results == TestResultConnection( - total_count=1, - edges=[ - { - "cursor": cursor(rows[0]), - "node": TestResultsRow(**rows[0]), - }, - ], - page_info={ - "has_next_page": False, - "has_previous_page": False, - "start_cursor": cursor(rows[0]), - "end_cursor": cursor(rows[0]), - }, - ) + assert test_results.total_count == 1 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] - def test_test_analytics_flag_filter(self, repository, store_in_redis, mock_storage): + def test_test_analytics_flag_filter( + self, repository, store_in_redis, mock_storage, snapshot + ): test_results = generate_test_results( repoid=repository.repoid, flags=[rows[0]["flags"][0]], @@ -524,21 +572,19 @@ def test_test_analytics_flag_filter(self, repository, store_in_redis, mock_stora measurement_interval=MeasurementInterval.INTERVAL_30_DAY, ) assert test_results is not None - assert test_results == TestResultConnection( - total_count=1, - edges=[ - { - "cursor": cursor(rows[0]), - "node": TestResultsRow(**rows[0]), - }, - ], - page_info={ - "has_next_page": False, - "has_previous_page": False, - "start_cursor": cursor(rows[0]), - "end_cursor": cursor(rows[0]), - }, - ) + # rows = dedup(rows) + assert test_results.total_count == 1 + assert test_results.page_info == { + "has_next_page": False, + "has_previous_page": False, + "start_cursor": cursor(rows[0]), + "end_cursor": cursor(rows[0]), + } + assert snapshot("json") == [ + row["node"].to_dict() + for row in test_results.edges + if isinstance(row["node"], TestResultsRow) + ] def test_gql_query(self, repository, store_in_redis, mock_storage): query = base_gql_query % ( @@ -580,7 +626,52 @@ def test_gql_query(self, repository, store_in_redis, mock_storage): "cursor": cursor(row), "node": row_to_camel_case(row), } - for row in reversed(rows) + for row in dedup(rows) + ] + + def test_gql_query_with_duplicate_names( + self, repository, store_in_redis_with_duplicate_names, mock_storage + ): + query = base_gql_query % ( + repository.author.username, + repository.name, + """ + testResults(ordering: { parameter: UPDATED_AT, direction: DESC } ) { + totalCount + edges { + cursor + node { + name + failureRate + flakeRate + updatedAt + avgDuration + totalFailCount + totalFlakyFailCount + totalPassCount + totalSkipCount + commitsFailed + lastDuration + } + } + } + """, + ) + + result = self.gql_request(query, owner=repository.author) + + assert ( + result["owner"]["repository"]["testAnalytics"]["testResults"]["totalCount"] + == 3 + ) + assert result["owner"]["repository"]["testAnalytics"]["testResults"][ + "edges" + ] == [ + { + "cursor": cursor(row), + "node": row_to_camel_case(row), + } + for row in dedup(rows_with_duplicate_names) ] def test_gql_query_aggregates(self, repository, store_in_redis, mock_storage): diff --git a/graphql_api/types/test_analytics/test_analytics.py b/graphql_api/types/test_analytics/test_analytics.py index 842b55610b..ed70412d15 100644 --- a/graphql_api/types/test_analytics/test_analytics.py +++ b/graphql_api/types/test_analytics/test_analytics.py @@ -52,6 +52,24 @@ class TestResultsRow: commits_where_fail: int last_duration: float + def to_dict(self) -> dict: + return { + "name": self.name, + "test_id": self.test_id, + "testsuite": self.testsuite, + "flags": self.flags, + "failure_rate": self.failure_rate, + "flake_rate": self.flake_rate, + "updated_at": self.updated_at.isoformat(), + "avg_duration": self.avg_duration, + "total_fail_count": self.total_fail_count, + "total_flaky_fail_count": self.total_flaky_fail_count, + "total_pass_count": self.total_pass_count, + "total_skip_count": self.total_skip_count, + "commits_where_fail": self.commits_where_fail, + "last_duration": self.last_duration, + } + @dataclass class TestResultConnection: @@ -197,12 +215,44 @@ def generate_test_results( }, ) + failure_rate_expr = ( + pl.col("failure_rate") + * (pl.col("total_fail_count") + pl.col("total_pass_count")) + ).sum() / (pl.col("total_fail_count") + pl.col("total_pass_count")).sum() + + flake_rate_expr = ( + pl.col("flake_rate") * (pl.col("total_fail_count") + pl.col("total_pass_count")) + ).sum() / (pl.col("total_fail_count") + pl.col("total_pass_count")).sum() + + avg_duration_expr = ( + pl.col("avg_duration") + * (pl.col("total_pass_count") + pl.col("total_fail_count")) + ).sum() / (pl.col("total_pass_count") + pl.col("total_fail_count")).sum() + + # dedup + table = table.group_by("name").agg( + pl.col("test_id").first().alias("test_id"), + pl.col("testsuite").alias("testsuite"), + pl.col("flags").explode().unique().alias("flags"), + failure_rate_expr.alias("failure_rate"), + flake_rate_expr.alias("flake_rate"), + pl.col("updated_at").max().alias("updated_at"), + avg_duration_expr.alias("avg_duration"), + pl.col("total_fail_count").sum().alias("total_fail_count"), + pl.col("total_flaky_fail_count").sum().alias("total_flaky_fail_count"), + pl.col("total_pass_count").sum().alias("total_pass_count"), + pl.col("total_skip_count").sum().alias("total_skip_count"), + pl.col("commits_where_fail").sum().alias("commits_where_fail"), + pl.col("last_duration").max().alias("last_duration"), + ) + if term: table = table.filter(pl.col("name").str.contains(term)) if testsuites: table = table.filter( - pl.col("testsuite").is_not_null() & pl.col("testsuite").is_in(testsuites) + pl.col("testsuite").is_not_null() + & pl.col("testsuite").list.eval(pl.element().is_in(testsuites)).list.any() ) if flags: diff --git a/pyproject.toml b/pyproject.toml index 6e3f87314f..dd49aa9159 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ dev-dependencies = [ "pytest-cov>=6.0.0", "pytest-asyncio>=0.14.0", "pytest-django==4.8.0", + "pytest-insta>=0.3.0", "pytest-mock==3.14.0", "pytest-split==0.10.0", "urllib3==1.26.19", diff --git a/uv.lock b/uv.lock index 5d2accda10..63c45b1b79 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '4' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", @@ -348,6 +347,7 @@ dependencies = [ { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "pyjwt" }, + { name = "pytest-insta" }, { name = "python-dateutil" }, { name = "python-json-logger" }, { name = "python-redis-lock" }, @@ -415,6 +415,7 @@ requires-dist = [ { name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "pydantic", specifier = ">=2.9.0" }, { name = "pyjwt", specifier = ">=2.4.0" }, + { name = "pytest-insta", specifier = ">=0.3.0" }, { name = "python-dateutil", specifier = "==2.9.0.post0" }, { name = "python-json-logger", specifier = "==2.0.7" }, { name = "python-redis-lock", specifier = "==4.0.0" }, @@ -1704,6 +1705,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/5b/29555191e903881d05e1f7184205ec534c7021e0ee077d1e6a1ee8f1b1eb/pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7", size = 23432 }, ] +[[package]] +name = "pytest-insta" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/5b/6ca4baca60c3f8361415501668cde3abd94dbad44293325833fd89d1a7c1/pytest_insta-0.3.0.tar.gz", hash = "sha256:9e6e1c70a021f68ccc4643360b2c2f8326cf3befba85f942c1da17b9caf713f7", size = 14960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/d5/1459b2861cf703cf49d96b6f29731ee74f4ac7e34b0c60b0ff75bdd318bc/pytest_insta-0.3.0-py3-none-any.whl", hash = "sha256:93a105e3850f2887b120a581923b10bb313d722e00d369377a1d91aa535df704", size = 13660 }, +] + [[package]] name = "pytest-mock" version = "3.14.0"