gitlab_quality-test_tooling 3.21.0 → 3.21.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/AGENTS.md +7 -0
- data/Gemfile.lock +1 -1
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql +22 -46
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator.rb +80 -28
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbd765eef13187839210bb0dde07988168415170236b8734d3faf396ce95b9a9
|
|
4
|
+
data.tar.gz: '0877e97fd029bdb06028dad82aeae1a15f18b73e3f8ad2ad9c0448819335ca71'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f242e07dbdefd766fd8f2bec91b779c93bf68f91a913314982b8e3053db99704fa0330cb31dadeb8829c570f43b7f2f04c52265fcdd75cc1beb60f1486096554
|
|
7
|
+
data.tar.gz: b6cd28e7ad33b233fb6f7c68db39ade7b72118913eac8e3b3281e018d59a9e9e5ca99b50a7730d28f189b5a2517c2c933e254d15f7a50cff6800476346aa4b37
|
data/AGENTS.md
CHANGED
|
@@ -75,6 +75,13 @@ Reference: https://docs.gitlab.com/ee/development/changelog.html
|
|
|
75
75
|
- **CodeCoverage** — Coverage analysis and responsibility patterns
|
|
76
76
|
- **ClickHouse::Client** — HTTP query/insert client for ClickHouse, used by the code coverage subsystem
|
|
77
77
|
|
|
78
|
+
## Code Comments
|
|
79
|
+
|
|
80
|
+
Comments carry the non-obvious why, not what the code already says. Keep them
|
|
81
|
+
short. A header block that restates the query or narrates each column gets
|
|
82
|
+
trimmed to the design decisions a reader can't infer from the code. This
|
|
83
|
+
applies to AI-generated comments too: trim before committing.
|
|
84
|
+
|
|
78
85
|
## Review Guidance
|
|
79
86
|
|
|
80
87
|
- `RelateFailureIssue#trigger_duo_analysis` deliberately rescues all `StandardError` (best-effort): a Duo/API failure must never block failure-issue creation. Preserve the broad rescue — don't narrow or remove it.
|
data/Gemfile.lock
CHANGED
data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql
CHANGED
|
@@ -1,52 +1,28 @@
|
|
|
1
|
-
-- Daily aggregation of per-test coverage
|
|
1
|
+
-- Daily aggregation of per-test coverage into per-group risk rows: one
|
|
2
|
+
-- (snapshot_date, group, stage, section) row inserted into
|
|
3
|
+
-- code_coverage.test_health_risk_per_group.
|
|
2
4
|
--
|
|
3
|
-
--
|
|
4
|
-
--
|
|
5
|
-
--
|
|
6
|
-
-- quarantined_union = union of line sets covered by *quarantined* tests
|
|
7
|
-
-- flaky_union = union of line sets covered by *flaky* tests
|
|
8
|
-
-- quarantined_or_flaky_union = union of line sets covered by *quarantined OR flaky* tests
|
|
9
|
-
-- healthy_union = union of line sets covered by tests that are neither
|
|
5
|
+
-- "At risk" lines are covered only by quarantined/flaky tests. The healthy
|
|
6
|
+
-- baseline excludes flaky tests by design: a flaky test isn't a reliable
|
|
7
|
+
-- safety net, so its lines shouldn't offset quarantine-only risk.
|
|
10
8
|
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
9
|
+
-- Parameters use `{name}` braces, substituted as literal text by gsub in
|
|
10
|
+
-- TestHealthRiskAggregator#build_sql after validation. A typoed placeholder
|
|
11
|
+
-- silently survives gsub, so match the names exactly.
|
|
12
|
+
-- {snapshot_date} date stamp for this run
|
|
13
|
+
-- {coverage_start} day of the most recent full per-test capture
|
|
14
|
+
-- {risk_window} quarantine/flaky lookback, default '30 DAY'
|
|
16
15
|
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
16
|
+
-- The INSERT is unconditional. Idempotency comes from the target table's
|
|
17
|
+
-- ReplacingMergeTree(version) engine keyed by the row tuple: re-runs and
|
|
18
|
+
-- overlapping cross-run inserts replace rather than duplicate, latest wins.
|
|
20
19
|
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
-- check. A typoed placeholder name will silently fall through gsub as a
|
|
28
|
-
-- literal — match the names exactly.
|
|
29
|
-
--
|
|
30
|
-
-- {snapshot_date} : the date stamp for this run, e.g. '2026-05-07'
|
|
31
|
-
-- {coverage_window} : interval to look back for fresh per-test rows,
|
|
32
|
-
-- default '2 DAY' (see Resilience note below)
|
|
33
|
-
-- {risk_window} : interval to consider quarantine/flaky status from
|
|
34
|
-
-- the test_metrics summaries, default '30 DAY'
|
|
35
|
-
|
|
36
|
-
-- Idempotency: this INSERT is unconditional. The target table must use
|
|
37
|
-
-- SharedReplacingMergeTree(version) (or ReplacingMergeTree on non-Cloud
|
|
38
|
-
-- ClickHouse; Cloud silently rewrites to Shared...) keyed by
|
|
39
|
-
-- (snapshot_date, group, stage, section), with
|
|
40
|
-
-- `version UInt64 MATERIALIZED toUnixTimestamp64Milli(now64(3))`, so re-running
|
|
41
|
-
-- for the same snapshot_date produces a higher version that replaces the
|
|
42
|
-
-- previous row on merge. Without that engine, re-runs would duplicate rows.
|
|
43
|
-
--
|
|
44
|
-
-- Resilience: `{coverage_window}` defaults to 2 DAY (see
|
|
45
|
-
-- TestHealthRiskAggregator::DEFAULT_COVERAGE_WINDOW). One missed nightly
|
|
46
|
-
-- run is recovered on the next night because the aggregation still sees
|
|
47
|
-
-- the previous day's per-test rows. Cross-run race: if two jobs land
|
|
48
|
-
-- inserts overlapping with the aggregation, the later aggregation wins
|
|
49
|
-
-- because of the version-based replacement.
|
|
20
|
+
-- Per-test capture is sparse between full sweeps (roughly weekly), so a
|
|
21
|
+
-- fixed trailing window would flap. {coverage_start} anchors the universe at
|
|
22
|
+
-- the last day that cleared the full-capture floor (#find_coverage_start);
|
|
23
|
+
-- when no day qualifies within the lookback, nothing is written and the
|
|
24
|
+
-- previous snapshot carries forward. FINAL on the source keeps the latest
|
|
25
|
+
-- version per (test_file, source_file), so the wide span never double-counts.
|
|
50
26
|
INSERT INTO code_coverage.test_health_risk_per_group
|
|
51
27
|
WITH
|
|
52
28
|
quarantine_status AS (
|
|
@@ -109,7 +85,7 @@ WITH
|
|
|
109
85
|
LEFT JOIN quarantine_status qs ON qs.test_file = tc.test_file
|
|
110
86
|
LEFT JOIN flaky_status fs ON fs.test_file = tc.test_file
|
|
111
87
|
LEFT JOIN test_ownership own ON own.file_path = tc.test_file
|
|
112
|
-
WHERE tc.timestamp >=
|
|
88
|
+
WHERE tc.timestamp >= toDate('{coverage_start}')
|
|
113
89
|
),
|
|
114
90
|
per_file AS (
|
|
115
91
|
SELECT
|
data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator.rb
CHANGED
|
@@ -19,14 +19,25 @@ module GitlabQuality
|
|
|
19
19
|
|
|
20
20
|
SQL_FILE = File.expand_path('test_health_risk_aggregation.sql', __dir__)
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
22
|
+
# Per-test capture is sparse between full captures (a full sweep of
|
|
23
|
+
# the suite lands roughly weekly; weekdays contribute only a small
|
|
24
|
+
# delta), so the coverage universe anchors at the most recent day
|
|
25
|
+
# whose capture volume clears this floor instead of a fixed trailing
|
|
26
|
+
# interval. Counted in distinct test files, because the weekend
|
|
27
|
+
# sweep enumerates the whole suite by test file: full sweeps land
|
|
28
|
+
# 22-27k test files, a single sweep bucket ~13k, and delta days at
|
|
29
|
+
# most ~16k even at their historical worst. Source files would not
|
|
30
|
+
# separate the cases: a few thousand tests already touch tens of
|
|
31
|
+
# thousands of source files through app boot, so a healthy delta
|
|
32
|
+
# day could pass a source-file floor and anchor the window at a
|
|
33
|
+
# partial capture.
|
|
34
|
+
DEFAULT_FULL_CAPTURE_FLOOR = 18_000
|
|
35
|
+
# Far enough back to bridge two missed weekly full captures before
|
|
36
|
+
# giving up, and well inside the source table's 90-day TTL. When no
|
|
37
|
+
# day within the lookback clears the floor, the run writes nothing
|
|
38
|
+
# so the previous snapshot carries forward rather than publishing a
|
|
39
|
+
# denominator built from sparse data.
|
|
40
|
+
DEFAULT_ANCHOR_LOOKBACK = '21 DAY'
|
|
30
41
|
DEFAULT_RISK_WINDOW = '30 DAY'
|
|
31
42
|
|
|
32
43
|
# `snapshot_date` is YYYY-MM-DD; intervals are `<integer> <unit>`
|
|
@@ -36,35 +47,38 @@ module GitlabQuality
|
|
|
36
47
|
|
|
37
48
|
def initialize(
|
|
38
49
|
url:, database:, username: nil, password: nil, logger: nil,
|
|
39
|
-
|
|
50
|
+
full_capture_floor: DEFAULT_FULL_CAPTURE_FLOOR, anchor_lookback: DEFAULT_ANCHOR_LOOKBACK,
|
|
51
|
+
risk_window: DEFAULT_RISK_WINDOW)
|
|
40
52
|
@url = url
|
|
41
53
|
@database = database
|
|
42
54
|
@username = username
|
|
43
55
|
@password = password
|
|
44
56
|
@logger = logger || ::Logger.new($stdout, level: 1)
|
|
45
|
-
@
|
|
57
|
+
@full_capture_floor = full_capture_floor
|
|
58
|
+
@anchor_lookback = anchor_lookback
|
|
46
59
|
@risk_window = risk_window
|
|
47
60
|
end
|
|
48
61
|
|
|
49
62
|
# @param snapshot_date [Date, String] date stamp for this run; defaults to today.
|
|
50
63
|
# @return [void]
|
|
51
64
|
def run(snapshot_date: Date.today) # rubocop:disable Metrics/AbcSize
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"coverage_window=#{coverage_window} risk_window=#{risk_window}"
|
|
56
|
-
)
|
|
57
|
-
client.query(sql, format: "TabSeparated")
|
|
58
|
-
inserted = fetch_row_count(snapshot_date)
|
|
59
|
-
if inserted.is_a?(Integer) && inserted.zero?
|
|
65
|
+
date = validate_date(snapshot_date)
|
|
66
|
+
coverage_start = find_coverage_start
|
|
67
|
+
if coverage_start.nil?
|
|
60
68
|
logger.warn(
|
|
61
|
-
"#{LOG_PREFIX}
|
|
62
|
-
"
|
|
63
|
-
"test_coverage_per_file directly if a recent export ran."
|
|
69
|
+
"#{LOG_PREFIX} No day within #{anchor_lookback} captured >= #{full_capture_floor} " \
|
|
70
|
+
"distinct test files; skipping snapshot for #{date} so the previous row carries forward."
|
|
64
71
|
)
|
|
65
|
-
|
|
66
|
-
logger.info("#{LOG_PREFIX} Aggregation wrote #{inserted} rows for snapshot_date=#{snapshot_date}")
|
|
72
|
+
return
|
|
67
73
|
end
|
|
74
|
+
|
|
75
|
+
sql = build_sql(snapshot_date: date, coverage_start: coverage_start)
|
|
76
|
+
logger.info(
|
|
77
|
+
"#{LOG_PREFIX} Running test_health_risk aggregation snapshot_date=#{date} " \
|
|
78
|
+
"coverage_start=#{coverage_start} risk_window=#{risk_window}"
|
|
79
|
+
)
|
|
80
|
+
client.query(sql, format: "TabSeparated")
|
|
81
|
+
report_row_count(date)
|
|
68
82
|
rescue StandardError => e
|
|
69
83
|
logger.error("#{LOG_PREFIX} Aggregation failed for #{snapshot_date}: #{e.message}")
|
|
70
84
|
raise
|
|
@@ -72,24 +86,62 @@ module GitlabQuality
|
|
|
72
86
|
|
|
73
87
|
private
|
|
74
88
|
|
|
75
|
-
attr_reader :url, :database, :username, :password, :logger,
|
|
89
|
+
attr_reader :url, :database, :username, :password, :logger,
|
|
90
|
+
:full_capture_floor, :anchor_lookback, :risk_window
|
|
91
|
+
|
|
92
|
+
# Most recent day whose capture volume looks like a full sweep of the
|
|
93
|
+
# suite. maxOrNull (rather than max) distinguishes "no qualifying day"
|
|
94
|
+
# from a real date when the subquery comes back empty.
|
|
95
|
+
def find_coverage_start
|
|
96
|
+
sql = <<~SQL
|
|
97
|
+
SELECT maxOrNull(day) AS coverage_start
|
|
98
|
+
FROM (
|
|
99
|
+
SELECT toDate(timestamp) AS day
|
|
100
|
+
FROM code_coverage.test_coverage_per_file
|
|
101
|
+
WHERE timestamp >= now() - INTERVAL #{validate_interval(anchor_lookback)}
|
|
102
|
+
GROUP BY day
|
|
103
|
+
HAVING uniqExact(test_file) >= #{validate_positive_integer(full_capture_floor)}
|
|
104
|
+
)
|
|
105
|
+
SQL
|
|
106
|
+
anchor = client.query(sql, format: "JSONCompact")&.dig('data', 0, 0)
|
|
107
|
+
anchor.nil? ? nil : validate_date(anchor, 'coverage_start')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def report_row_count(date)
|
|
111
|
+
inserted = fetch_row_count(date)
|
|
112
|
+
if inserted.is_a?(Integer) && inserted.zero?
|
|
113
|
+
logger.warn(
|
|
114
|
+
"#{LOG_PREFIX} Aggregation wrote 0 rows for snapshot_date=#{date}. " \
|
|
115
|
+
"This is valid if no per-test data landed since the coverage anchor, but worth " \
|
|
116
|
+
"checking test_coverage_per_file directly if a recent export ran."
|
|
117
|
+
)
|
|
118
|
+
else
|
|
119
|
+
logger.info("#{LOG_PREFIX} Aggregation wrote #{inserted} rows for snapshot_date=#{date}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
76
122
|
|
|
77
|
-
def build_sql(snapshot_date:)
|
|
123
|
+
def build_sql(snapshot_date:, coverage_start:)
|
|
78
124
|
template = File.read(SQL_FILE)
|
|
79
125
|
template
|
|
80
126
|
.gsub('{snapshot_date}', validate_date(snapshot_date))
|
|
81
|
-
.gsub('{
|
|
127
|
+
.gsub('{coverage_start}', coverage_start)
|
|
82
128
|
.gsub('{risk_window}', validate_interval(risk_window))
|
|
83
129
|
end
|
|
84
130
|
|
|
85
131
|
# DateTime/Time `to_s` includes the time portion and is rejected.
|
|
86
|
-
def validate_date(value)
|
|
132
|
+
def validate_date(value, name = 'snapshot_date')
|
|
87
133
|
str = value.to_s
|
|
88
|
-
raise ArgumentError, "Invalid
|
|
134
|
+
raise ArgumentError, "Invalid #{name}: #{value.inspect}" unless DATE_PATTERN.match?(str)
|
|
89
135
|
|
|
90
136
|
str
|
|
91
137
|
end
|
|
92
138
|
|
|
139
|
+
def validate_positive_integer(value)
|
|
140
|
+
raise ArgumentError, "Invalid full_capture_floor: #{value.inspect}" unless value.is_a?(Integer) && value.positive?
|
|
141
|
+
|
|
142
|
+
value.to_s
|
|
143
|
+
end
|
|
144
|
+
|
|
93
145
|
def validate_interval(value)
|
|
94
146
|
raise ArgumentError, "Invalid interval expression: #{value.inspect}" unless INTERVAL_PATTERN.match?(value.to_s)
|
|
95
147
|
|
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.21.
|
|
4
|
+
version: 3.21.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-08-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|