gitlab_quality-test_tooling 3.18.3 → 3.19.0
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 +6 -11
- 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 +19 -1
- 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: de285d350eb535db983a229c6fbe2ea8565fd92ac757ba4371a7abf2e86a0e44
|
|
4
|
+
data.tar.gz: bb1143033fdd4d8e0c71c4a0ca27574391138feed3b5198f934df1356684c866
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43d770b36e5b946f287a5f309f0e242033432c9b9a2f7618e396abc894067314d26a0b88416c41f880d1a16c5ffe7a595e306f074a057478fb139f2361e77e56
|
|
7
|
+
data.tar.gz: 8f61352e3441e40c2b09d45621e91028f8d1f48623859ba34f5aabc7db449b03bc80f18ede9cbb558c47776d4a6b10c7fdb8e39e22b1dd0158477f2d85d97d8c
|
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,13 +71,6 @@ WITH
|
|
|
71
71
|
GROUP BY test_file
|
|
72
72
|
HAVING is_flaky
|
|
73
73
|
),
|
|
74
|
-
-- Jest quarantine comes from a flat file (quarantined_vue3_specs.txt) rather
|
|
75
|
-
-- than the issue-based RSpec ledger. It is populated into this helper table
|
|
76
|
-
-- by JestQuarantinedTestsTable before each nightly aggregation run.
|
|
77
|
-
jest_quarantine_status AS (
|
|
78
|
-
SELECT test_file
|
|
79
|
-
FROM code_coverage.jest_quarantined_tests_today FINAL
|
|
80
|
-
),
|
|
81
74
|
per_test_status AS (
|
|
82
75
|
SELECT
|
|
83
76
|
tc.source_file,
|
|
@@ -86,12 +79,14 @@ WITH
|
|
|
86
79
|
tc.section,
|
|
87
80
|
tc.total_lines,
|
|
88
81
|
tc.covered_lines,
|
|
89
|
-
|
|
82
|
+
-- Jest quarantine is a flat list (quarantined_vue3_specs.txt), loaded into
|
|
83
|
+
-- jest_quarantined_tests_today by JestQuarantinedTestsTable before each run.
|
|
84
|
+
(coalesce(qs.is_quarantined, FALSE)
|
|
85
|
+
OR tc.test_file IN (SELECT test_file FROM code_coverage.jest_quarantined_tests_today FINAL)) AS is_quarantined,
|
|
90
86
|
coalesce(fs.is_flaky, FALSE) AS is_flaky
|
|
91
87
|
FROM code_coverage.test_coverage_per_file tc FINAL
|
|
92
|
-
LEFT JOIN quarantine_status
|
|
93
|
-
LEFT JOIN
|
|
94
|
-
LEFT JOIN flaky_status fs ON fs.test_file = tc.test_file
|
|
88
|
+
LEFT JOIN quarantine_status qs ON qs.test_file = tc.test_file
|
|
89
|
+
LEFT JOIN flaky_status fs ON fs.test_file = tc.test_file
|
|
95
90
|
WHERE tc.timestamp >= now() - INTERVAL {coverage_window}
|
|
96
91
|
),
|
|
97
92
|
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,6 +115,21 @@ 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
134
|
# carry blank categories. `CategoryOwners` fetches stages.yml over HTTP,
|
|
117
135
|
# so only build it when there are reports to enrich.
|
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.0
|
|
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-06-
|
|
11
|
+
date: 2026-06-27 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
|