gitlab_quality-test_tooling 3.18.3 → 3.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/exe/test-coverage +8 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql +33 -16
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshot.sql +31 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter.rb +95 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_exporter.rb +34 -4
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fb5356dd9a684e76ccf4de875f66d77ee818bef86d1cd9792eb8db9d688cfe9
|
|
4
|
+
data.tar.gz: 75e5afd55644a124fbf09f4fe0edd8f298fb2614f54dd0884a8a18002d2fe6d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35ff364a8fbbd437bef5e94a6bedccf66b8288965fd6c136ce6c21baa3d9052ac44281e54dd3a11eb92dae156f6e9e123756fa526ddb24fd39c00450b1d7187e
|
|
7
|
+
data.tar.gz: 1de79819dcb8aadf7ef3994f54387008ff9d1e15ea4a0b0590caebe9cf2af99a2821dcc7008b7bff5b2df9da69d4103622624983ce7434272ec2e5663c1e6695
|
data/Gemfile.lock
CHANGED
data/exe/test-coverage
CHANGED
|
@@ -14,6 +14,7 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/p
|
|
|
14
14
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table'
|
|
15
15
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/jest_quarantined_tests_table'
|
|
16
16
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator'
|
|
17
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter'
|
|
17
18
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data'
|
|
18
19
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
19
20
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
@@ -177,6 +178,13 @@ clickhouse_data = {
|
|
|
177
178
|
case mode
|
|
178
179
|
when :aggregation
|
|
179
180
|
GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestHealthRiskAggregator.new(**clickhouse_data).run
|
|
181
|
+
# Best-effort: the score snapshot is a non-critical add-on, so it must not
|
|
182
|
+
# fail the job after the aggregation has already succeeded.
|
|
183
|
+
begin
|
|
184
|
+
GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestHealthScoreSnapshotter.new(**clickhouse_data).run
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
warn "Warning: score snapshot failed (aggregation succeeded), continuing: #{e.message}"
|
|
187
|
+
end
|
|
180
188
|
when :per_test
|
|
181
189
|
GitlabQuality::TestTooling::CodeCoverage::PerTestCoverageExporter.new(
|
|
182
190
|
coverage_glob: params[:per_test_coverage],
|
data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql
CHANGED
|
@@ -71,27 +71,44 @@ WITH
|
|
|
71
71
|
GROUP BY test_file
|
|
72
72
|
HAVING is_flaky
|
|
73
73
|
),
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
test_ownership AS (
|
|
75
|
+
-- Authoritative test-file ownership from test_metrics, which resolves
|
|
76
|
+
-- feature_category to group/stage/section from a current source. The
|
|
77
|
+
-- per-test coverage export's own CategoryOwners mapping is stale (it fetches
|
|
78
|
+
-- a dated stages.yml), so many rows arrive with a blank group; this is the
|
|
79
|
+
-- fallback. argMax pins the latest ownership per file over the risk window.
|
|
80
|
+
SELECT
|
|
81
|
+
file_path,
|
|
82
|
+
argMax(`group`, timestamp) AS owner_group,
|
|
83
|
+
argMax(stage, timestamp) AS owner_stage,
|
|
84
|
+
argMax(section, timestamp) AS owner_section
|
|
85
|
+
FROM test_metrics.test_results
|
|
86
|
+
WHERE timestamp >= now() - INTERVAL {risk_window}
|
|
87
|
+
AND `group` NOT IN ('', 'unknown')
|
|
88
|
+
GROUP BY file_path
|
|
80
89
|
),
|
|
81
90
|
per_test_status AS (
|
|
82
91
|
SELECT
|
|
83
|
-
tc.source_file,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
tc
|
|
88
|
-
|
|
89
|
-
(
|
|
92
|
+
tc.source_file AS source_file,
|
|
93
|
+
-- Fall back to test_metrics ownership when the coverage row has no group
|
|
94
|
+
-- (stale CategoryOwners mapping). Resolve all three off the same condition
|
|
95
|
+
-- so the team stays internally consistent. Alias the passthrough columns to
|
|
96
|
+
-- bare names too: the analyzer otherwise keeps the `tc.` qualifier once the
|
|
97
|
+
-- ownership join is in play, and per_file references them unqualified.
|
|
98
|
+
if(tc.`group` != '', tc.`group`, own.owner_group) AS `group`,
|
|
99
|
+
if(tc.`group` != '', tc.stage, own.owner_stage) AS stage,
|
|
100
|
+
if(tc.`group` != '', tc.section, own.owner_section) AS section,
|
|
101
|
+
tc.total_lines AS total_lines,
|
|
102
|
+
tc.covered_lines AS covered_lines,
|
|
103
|
+
-- Jest quarantine is a flat list (quarantined_vue3_specs.txt), loaded into
|
|
104
|
+
-- jest_quarantined_tests_today by JestQuarantinedTestsTable before each run.
|
|
105
|
+
(coalesce(qs.is_quarantined, FALSE)
|
|
106
|
+
OR tc.test_file IN (SELECT test_file FROM code_coverage.jest_quarantined_tests_today FINAL)) AS is_quarantined,
|
|
90
107
|
coalesce(fs.is_flaky, FALSE) AS is_flaky
|
|
91
108
|
FROM code_coverage.test_coverage_per_file tc FINAL
|
|
92
|
-
LEFT JOIN quarantine_status
|
|
93
|
-
LEFT JOIN
|
|
94
|
-
LEFT JOIN
|
|
109
|
+
LEFT JOIN quarantine_status qs ON qs.test_file = tc.test_file
|
|
110
|
+
LEFT JOIN flaky_status fs ON fs.test_file = tc.test_file
|
|
111
|
+
LEFT JOIN test_ownership own ON own.file_path = tc.test_file
|
|
95
112
|
WHERE tc.timestamp >= now() - INTERVAL {coverage_window}
|
|
96
113
|
),
|
|
97
114
|
per_file AS (
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Freezes today's per-pillar Test Health Index scores into the history table.
|
|
2
|
+
--
|
|
3
|
+
-- Reads one pillar's score view (the same view the dashboard reads, so the
|
|
4
|
+
-- snapshot and the dashboard cannot diverge) and tags each row with the
|
|
5
|
+
-- snapshot date and the pillar. Run once per pillar by
|
|
6
|
+
-- TestHealthScoreSnapshotter, right after the daily risk aggregation refreshes
|
|
7
|
+
-- test_health_risk_per_group, which the view reads.
|
|
8
|
+
--
|
|
9
|
+
-- Idempotency: the target is a SharedReplacingMergeTree keyed by
|
|
10
|
+
-- (snapshot_date, level, group, stage, section, pillar) with a version column,
|
|
11
|
+
-- so re-running for the same snapshot_date upserts and the last run of the day
|
|
12
|
+
-- wins on merge. No "final run of the day" detection is needed.
|
|
13
|
+
--
|
|
14
|
+
-- Parameter substitution uses {name} braces, replaced via gsub in
|
|
15
|
+
-- TestHealthScoreSnapshotter#build_sql after each value passes a regex check:
|
|
16
|
+
-- {snapshot_date} : the date stamp for this run, e.g. '2026-05-07'
|
|
17
|
+
-- {pillar} : quarantine | slow (names the view and tags the rows)
|
|
18
|
+
INSERT INTO code_coverage.test_health_score_history
|
|
19
|
+
(snapshot_date, level, `group`, stage, section, pillar, score, rag_status, formula_version, factors)
|
|
20
|
+
SELECT
|
|
21
|
+
toDate('{snapshot_date}') AS snapshot_date,
|
|
22
|
+
level,
|
|
23
|
+
`group`,
|
|
24
|
+
stage,
|
|
25
|
+
section,
|
|
26
|
+
'{pillar}' AS pillar,
|
|
27
|
+
score,
|
|
28
|
+
rag_status,
|
|
29
|
+
formula_version,
|
|
30
|
+
factors
|
|
31
|
+
FROM code_coverage.test_health_scores_{pillar};
|
data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'client'
|
|
4
|
+
require_relative 'table'
|
|
5
|
+
|
|
6
|
+
module GitlabQuality
|
|
7
|
+
module TestTooling
|
|
8
|
+
module CodeCoverage
|
|
9
|
+
module ClickHouse
|
|
10
|
+
# Freezes the day's per-pillar Test Health Index scores into
|
|
11
|
+
# `code_coverage.test_health_score_history`. Reads the per-pillar score
|
|
12
|
+
# views (the same views the dashboard reads, so the snapshot cannot
|
|
13
|
+
# diverge from the live score) and tags each row with the snapshot date
|
|
14
|
+
# and the pillar.
|
|
15
|
+
#
|
|
16
|
+
# Runs right after TestHealthRiskAggregator in the daily job: the views
|
|
17
|
+
# read `test_health_risk_per_group`, which the aggregator has just
|
|
18
|
+
# refreshed, so the freeze captures the current day. The producer never
|
|
19
|
+
# holds the score formula; it copies whatever the views compute.
|
|
20
|
+
class TestHealthScoreSnapshotter
|
|
21
|
+
include Client
|
|
22
|
+
|
|
23
|
+
SQL_FILE = File.expand_path('test_health_score_snapshot.sql', __dir__)
|
|
24
|
+
|
|
25
|
+
# One INSERT per pillar. The token names the view
|
|
26
|
+
# (test_health_scores_<pillar>) and tags the row's `pillar` column.
|
|
27
|
+
# Flaky is excluded until it is scored live.
|
|
28
|
+
PILLARS = %w[quarantine slow].freeze
|
|
29
|
+
|
|
30
|
+
DATE_PATTERN = /\A\d{4}-\d{2}-\d{2}\z/
|
|
31
|
+
|
|
32
|
+
def initialize(url:, database:, username: nil, password: nil, logger: nil)
|
|
33
|
+
@url = url
|
|
34
|
+
@database = database
|
|
35
|
+
@username = username
|
|
36
|
+
@password = password
|
|
37
|
+
@logger = logger || ::Logger.new($stdout, level: ::Logger::INFO)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param snapshot_date [Date, String] date stamp for this run; defaults to today.
|
|
41
|
+
# @return [void]
|
|
42
|
+
def run(snapshot_date: Date.today)
|
|
43
|
+
PILLARS.each do |pillar|
|
|
44
|
+
logger.info("#{LOG_PREFIX} Snapshotting #{pillar} scores snapshot_date=#{snapshot_date}")
|
|
45
|
+
client.query(build_sql(snapshot_date: snapshot_date, pillar: pillar), format: "TabSeparated")
|
|
46
|
+
end
|
|
47
|
+
inserted = fetch_row_count(snapshot_date)
|
|
48
|
+
logger.info("#{LOG_PREFIX} Score snapshot wrote #{inserted} rows for snapshot_date=#{snapshot_date}")
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
logger.error("#{LOG_PREFIX} Score snapshot failed for #{snapshot_date}: #{e.message}")
|
|
51
|
+
raise
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :url, :database, :username, :password, :logger
|
|
57
|
+
|
|
58
|
+
def build_sql(snapshot_date:, pillar:)
|
|
59
|
+
File.read(SQL_FILE)
|
|
60
|
+
.gsub('{snapshot_date}', validate_date(snapshot_date))
|
|
61
|
+
.gsub('{pillar}', validate_pillar(pillar))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# DateTime/Time `to_s` includes the time portion and is rejected.
|
|
65
|
+
def validate_date(value)
|
|
66
|
+
str = value.to_s
|
|
67
|
+
raise ArgumentError, "Invalid snapshot_date: #{value.inspect}" unless DATE_PATTERN.match?(str)
|
|
68
|
+
|
|
69
|
+
str
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The pillar names the view and is interpolated into the SQL, so
|
|
73
|
+
# restrict it to the known set rather than a looser pattern.
|
|
74
|
+
def validate_pillar(value)
|
|
75
|
+
raise ArgumentError, "Invalid pillar: #{value.inspect}" unless PILLARS.include?(value)
|
|
76
|
+
|
|
77
|
+
value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns 'unknown' on any error so a transient count-query failure
|
|
81
|
+
# can't mask the success of the actual INSERTs.
|
|
82
|
+
def fetch_row_count(snapshot_date)
|
|
83
|
+
count_sql = "SELECT count() FROM code_coverage.test_health_score_history FINAL " \
|
|
84
|
+
"WHERE snapshot_date = toDate('#{validate_date(snapshot_date)}')"
|
|
85
|
+
result = client.query(count_sql, format: "JSONCompact")
|
|
86
|
+
result&.dig('data', 0, 0) || 'unknown'
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
logger.debug("#{LOG_PREFIX} Could not fetch post-snapshot row count: #{e.message}")
|
|
89
|
+
'unknown'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -52,7 +52,10 @@ module GitlabQuality
|
|
|
52
52
|
|
|
53
53
|
insert(coverage_files)
|
|
54
54
|
jest_quarantine_error = ingest_jest_quarantine_best_effort
|
|
55
|
-
|
|
55
|
+
unless skip_aggregation
|
|
56
|
+
aggregate
|
|
57
|
+
snapshot_scores
|
|
58
|
+
end
|
|
56
59
|
|
|
57
60
|
# Re-raise only after aggregation has run, so a jest quarantine ingest
|
|
58
61
|
# failure can't silently stop the table-wide rollup while still
|
|
@@ -112,9 +115,24 @@ module GitlabQuality
|
|
|
112
115
|
ClickHouse::TestHealthRiskAggregator.new(**clickhouse_credentials).run
|
|
113
116
|
end
|
|
114
117
|
|
|
118
|
+
# Freezes the day's per-pillar scores into test_health_score_history,
|
|
119
|
+
# right after the aggregation the score views read. Best-effort: the
|
|
120
|
+
# snapshot is a non-critical add-on, so a failure (for example before
|
|
121
|
+
# the score views exist) must not fail the daily job once the
|
|
122
|
+
# aggregation has succeeded. A missed freeze is recoverable because the
|
|
123
|
+
# dashboard reads the live view for today.
|
|
124
|
+
def snapshot_scores
|
|
125
|
+
ClickHouse::TestHealthScoreSnapshotter.new(**clickhouse_credentials).run
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
logger.error(
|
|
128
|
+
"#{ClickHouse::LOG_PREFIX} Score snapshot failed; the aggregation still " \
|
|
129
|
+
"succeeded, continuing: #{e.message}"
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
115
133
|
# `tests_to_categories` comes from the test report JSON; without it, rows
|
|
116
|
-
# carry blank categories.
|
|
117
|
-
# so only build
|
|
134
|
+
# carry blank categories. The category-to-team map is a ClickHouse read,
|
|
135
|
+
# so only build both when there are reports to enrich.
|
|
118
136
|
def resolve_categories
|
|
119
137
|
return [{}, {}] unless test_reports
|
|
120
138
|
|
|
@@ -123,7 +141,19 @@ module GitlabQuality
|
|
|
123
141
|
report_files = Dir.glob(patterns)
|
|
124
142
|
return [{}, {}] if report_files.empty?
|
|
125
143
|
|
|
126
|
-
[tests_to_categories_from(report_files),
|
|
144
|
+
[tests_to_categories_from(report_files), feature_categories_to_teams]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Category to group/stage/section, read from `code_coverage.category_owners`
|
|
148
|
+
# (populated by sync-category-owners). This is the source the full
|
|
149
|
+
# coverage export already uses, and it retains categories the live
|
|
150
|
+
# stages.yml fetch has since dropped (global_search and the Duo/AI groups),
|
|
151
|
+
# so more rows resolve to a real group instead of blank. `owner_records`
|
|
152
|
+
# gives string-keyed values; PerTestCoverageData reads symbol keys.
|
|
153
|
+
def feature_categories_to_teams
|
|
154
|
+
ClickHouse::CategoryOwnersTable.new(**clickhouse_credentials).owner_records.transform_values do |owner|
|
|
155
|
+
{ group: owner["group"], stage: owner["stage"], section: owner["section"] }
|
|
156
|
+
end
|
|
127
157
|
end
|
|
128
158
|
|
|
129
159
|
def tests_to_categories_from(report_files)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab_quality-test_tooling
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.19.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab Quality
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -499,6 +499,8 @@ files:
|
|
|
499
499
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb
|
|
500
500
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql
|
|
501
501
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator.rb
|
|
502
|
+
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshot.sql
|
|
503
|
+
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter.rb
|
|
502
504
|
- lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb
|
|
503
505
|
- lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
|
|
504
506
|
- lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_data.rb
|