gitlab_quality-test_tooling 3.20.2 → 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/exe/test-coverage +21 -3
- 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/code_coverage/per_test_coverage_exporter.rb +20 -10
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +0 -9
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +0 -5
- data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +4 -5
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +2 -2
- data/lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb +0 -2
- data/lib/gitlab_quality/test_tooling/test_result/json_test_result.rb +0 -8
- 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/exe/test-coverage
CHANGED
|
@@ -73,7 +73,9 @@ options = OptionParser.new do |opts|
|
|
|
73
73
|
'Inserts rows into code_coverage.test_coverage_per_file and runs the daily ' \
|
|
74
74
|
'test_health_risk aggregation unless --skip-aggregation. Needs only the ' \
|
|
75
75
|
'--clickhouse-* options; --test-reports and --jest-quarantine-file are ' \
|
|
76
|
-
'optional enrichment.
|
|
76
|
+
'optional enrichment. --test-reports also requires --clickhouse-shared-database, ' \
|
|
77
|
+
'since category ownership is read from the shared database. ' \
|
|
78
|
+
'(e.g., "tmp/per-test-coverage-*.ndjson")') do |pattern|
|
|
77
79
|
params[:per_test_coverage] = pattern
|
|
78
80
|
end
|
|
79
81
|
|
|
@@ -135,7 +137,16 @@ mode =
|
|
|
135
137
|
clickhouse_required = [:clickhouse_url, :clickhouse_database, :clickhouse_username]
|
|
136
138
|
full_export_required =
|
|
137
139
|
[:test_reports, :coverage_report, :test_map, :responsibility_patterns, :clickhouse_shared_database] + clickhouse_required
|
|
138
|
-
|
|
140
|
+
# Per-test mode reads category_owners from the shared database only when it enriches
|
|
141
|
+
# rows (--test-reports present), so require the shared database exactly then.
|
|
142
|
+
per_test_enriching = mode == :per_test && params[:test_reports]
|
|
143
|
+
required_params =
|
|
144
|
+
case mode
|
|
145
|
+
when :full_export
|
|
146
|
+
full_export_required
|
|
147
|
+
else
|
|
148
|
+
per_test_enriching ? clickhouse_required + [:clickhouse_shared_database] : clickhouse_required
|
|
149
|
+
end
|
|
139
150
|
|
|
140
151
|
unless params.any? && (required_params - params.keys).none?
|
|
141
152
|
puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
|
|
@@ -149,7 +160,13 @@ if clickhouse_password.to_s.strip.empty?
|
|
|
149
160
|
exit 1
|
|
150
161
|
end
|
|
151
162
|
|
|
152
|
-
non_empty_params =
|
|
163
|
+
non_empty_params =
|
|
164
|
+
if mode == :full_export || per_test_enriching
|
|
165
|
+
clickhouse_required + [:clickhouse_shared_database]
|
|
166
|
+
else
|
|
167
|
+
clickhouse_required
|
|
168
|
+
end
|
|
169
|
+
|
|
153
170
|
non_empty_params.each do |param|
|
|
154
171
|
next unless params[param].to_s.strip.empty?
|
|
155
172
|
|
|
@@ -189,6 +206,7 @@ when :per_test
|
|
|
189
206
|
GitlabQuality::TestTooling::CodeCoverage::PerTestCoverageExporter.new(
|
|
190
207
|
coverage_glob: params[:per_test_coverage],
|
|
191
208
|
clickhouse: clickhouse_data,
|
|
209
|
+
clickhouse_shared_database: params[:clickhouse_shared_database],
|
|
192
210
|
test_reports: params[:test_reports],
|
|
193
211
|
jest_quarantine_file: params[:jest_quarantine_file],
|
|
194
212
|
skip_aggregation: params.fetch(:skip_aggregation, false)
|
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
|
|
|
@@ -23,12 +23,15 @@ module GitlabQuality
|
|
|
23
23
|
# aggregation once at the end.
|
|
24
24
|
class PerTestCoverageExporter
|
|
25
25
|
# @param clickhouse [Hash] connection params (:url, :database, :username, :password)
|
|
26
|
+
# @param clickhouse_shared_database [String, nil] the shared database holding the
|
|
27
|
+
# canonical `category_owners`. Required only when enriching (test_reports present).
|
|
26
28
|
def initialize(
|
|
27
|
-
coverage_glob:, clickhouse:,
|
|
29
|
+
coverage_glob:, clickhouse:, clickhouse_shared_database: nil,
|
|
28
30
|
test_reports: nil, jest_quarantine_file: nil,
|
|
29
31
|
captured_sha: ENV.fetch('CI_COMMIT_SHA', ''), skip_aggregation: false, logger: nil)
|
|
30
32
|
@coverage_glob = coverage_glob
|
|
31
33
|
@clickhouse = clickhouse
|
|
34
|
+
@clickhouse_shared_database = clickhouse_shared_database
|
|
32
35
|
@test_reports = test_reports
|
|
33
36
|
@jest_quarantine_file = jest_quarantine_file
|
|
34
37
|
@captured_sha = captured_sha.to_s
|
|
@@ -65,8 +68,8 @@ module GitlabQuality
|
|
|
65
68
|
|
|
66
69
|
private
|
|
67
70
|
|
|
68
|
-
attr_reader :coverage_glob, :clickhouse, :
|
|
69
|
-
:captured_sha, :skip_aggregation, :logger
|
|
71
|
+
attr_reader :coverage_glob, :clickhouse, :clickhouse_shared_database, :test_reports,
|
|
72
|
+
:jest_quarantine_file, :captured_sha, :skip_aggregation, :logger
|
|
70
73
|
|
|
71
74
|
def insert(coverage_files)
|
|
72
75
|
tests_to_categories, feature_categories_to_teams = resolve_categories
|
|
@@ -144,14 +147,21 @@ module GitlabQuality
|
|
|
144
147
|
[tests_to_categories_from(report_files), feature_categories_to_teams]
|
|
145
148
|
end
|
|
146
149
|
|
|
147
|
-
# Category to group/stage/section, read from `
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
150
|
+
# Category to group/stage/section, read from `category_owners` in the shared
|
|
151
|
+
# database, the same source the full coverage export uses. The shared copy is
|
|
152
|
+
# refreshed by sync-category-owners; the legacy `code_coverage.category_owners`
|
|
153
|
+
# stopped updating in December 2025, so reading it would drop every category
|
|
154
|
+
# added since. `owner_records` gives string-keyed values; PerTestCoverageData
|
|
155
|
+
# reads symbol keys.
|
|
153
156
|
def feature_categories_to_teams
|
|
154
|
-
|
|
157
|
+
if clickhouse_shared_database.to_s.strip.empty?
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
'clickhouse_shared_database is required to enrich per-test rows with category owners'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
shared_credentials = clickhouse_credentials.merge(database: clickhouse_shared_database)
|
|
163
|
+
|
|
164
|
+
ClickHouse::CategoryOwnersTable.new(**shared_credentials).owner_records.transform_values do |owner|
|
|
155
165
|
{ group: owner["group"], stage: owner["stage"], section: owner["section"] }
|
|
156
166
|
end
|
|
157
167
|
end
|
|
@@ -151,7 +151,6 @@ module GitlabQuality
|
|
|
151
151
|
line_number: extract_line_number(test),
|
|
152
152
|
exception: extract_exception_data(test),
|
|
153
153
|
ci_job_url: test.ci_job_url,
|
|
154
|
-
testcase: extract_test_id_or_name(test),
|
|
155
154
|
product_group: extract_product_group(test),
|
|
156
155
|
level: extract_level(test)
|
|
157
156
|
}
|
|
@@ -184,14 +183,6 @@ module GitlabQuality
|
|
|
184
183
|
}
|
|
185
184
|
end
|
|
186
185
|
|
|
187
|
-
def extract_test_id_or_name(test)
|
|
188
|
-
return test.example_id if test.respond_to?(:example_id)
|
|
189
|
-
return test.id if test.respond_to?(:id)
|
|
190
|
-
return test.name if test.respond_to?(:name)
|
|
191
|
-
|
|
192
|
-
"#{test.relative_file}:#{test.respond_to?(:line_number) ? test.line_number : 'unknown'}"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
186
|
def new_issue_labels(test)
|
|
196
187
|
up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS + group_and_category_labels_for_test(test))
|
|
197
188
|
end
|
|
@@ -63,7 +63,6 @@ module GitlabQuality
|
|
|
63
63
|
| Test level | `#{test.level}` |
|
|
64
64
|
| Hash | `#{test_hash(test)}` |
|
|
65
65
|
| Max expected duration | < #{test.max_duration_for_test} seconds |
|
|
66
|
-
#{"| Test case | #{test.testcase} |" if test.testcase}
|
|
67
66
|
<!-- Don't modify this section as it's auto-generated -->
|
|
68
67
|
DESCRIPTION
|
|
69
68
|
end
|
|
@@ -49,16 +49,11 @@ module GitlabQuality
|
|
|
49
49
|
|
|
50
50
|
report_test(test)
|
|
51
51
|
end
|
|
52
|
-
|
|
53
|
-
test_results.write
|
|
54
52
|
end
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
def report_test(test)
|
|
58
56
|
testcase = testcase_project_reporter.find_or_create_testcase(test)
|
|
59
|
-
# The API returns the test case with an issue URL since it is technically a type of issue.
|
|
60
|
-
# This updates the URL to a valid test case link.
|
|
61
|
-
test.testcase = testcase.web_url.sub('/issues/', '/quality/test_cases/')
|
|
62
57
|
|
|
63
58
|
result_issue, is_new = results_issue_project_reporter.get_related_issue(testcase, test)
|
|
64
59
|
|
|
@@ -18,18 +18,17 @@ module GitlabQuality
|
|
|
18
18
|
find_testcase(test) || create_issue(test)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def add_result_issue_link_to_testcase(testcase, result_issue,
|
|
21
|
+
def add_result_issue_link_to_testcase(testcase, result_issue, _test)
|
|
22
22
|
results_section = testcase.description.include?(TEST_CASE_RESULTS_SECTION_TEMPLATE) ? '' : TEST_CASE_RESULTS_SECTION_TEMPLATE
|
|
23
23
|
|
|
24
24
|
gitlab.edit_issue(iid: testcase.iid,
|
|
25
25
|
options: { description: (testcase.description + results_section + "\n\n#{result_issue.web_url}") })
|
|
26
|
-
|
|
27
|
-
puts "Added results issue #{result_issue.web_url} link to test case #{test.testcase}"
|
|
26
|
+
puts "Added results issue #{result_issue.web_url} link to test case #{testcase.web_url}"
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def update_testcase(testcase, test)
|
|
31
|
-
puts "Labels updated for test case #{
|
|
32
|
-
puts "Quarantine section updated for test case #{
|
|
30
|
+
puts "Labels updated for test case #{testcase.web_url}." if update_labels(testcase, test)
|
|
31
|
+
puts "Quarantine section updated for test case #{testcase.web_url}." if update_quarantine_section(testcase, test)
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
private
|
|
@@ -37,7 +37,7 @@ module GitlabQuality
|
|
|
37
37
|
description_start_index + description_length
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# List specs in markdown with details such as link to code,
|
|
40
|
+
# List specs in markdown with details such as link to code, metrics and failure issue
|
|
41
41
|
#
|
|
42
42
|
# @param [Hash<String,Hash>] commits The commits hash to use for spec details
|
|
43
43
|
# @return String
|
|
@@ -45,7 +45,7 @@ module GitlabQuality
|
|
|
45
45
|
commits.each_with_index.map do |(changed_line_number, spec), index|
|
|
46
46
|
<<~MARKDOWN
|
|
47
47
|
#{index + 1}. [`#{spec['name']}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{spec['file_path']}#L#{changed_line_number.to_i + 1})
|
|
48
|
-
| [
|
|
48
|
+
| [Spec metrics](#{context.single_spec_metrics_link(spec['file_path'])})
|
|
49
49
|
#{failure_issue_text(spec)}
|
|
50
50
|
MARKDOWN
|
|
51
51
|
end.join("\n")
|
|
@@ -60,14 +60,6 @@ module GitlabQuality
|
|
|
60
60
|
report.fetch('ci_job_url', '')
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def testcase
|
|
64
|
-
report.fetch('testcase', '')
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def testcase=(new_testcase)
|
|
68
|
-
report['testcase'] = new_testcase
|
|
69
|
-
end
|
|
70
|
-
|
|
71
63
|
def quarantine_type
|
|
72
64
|
quarantine['type'] if quarantine?
|
|
73
65
|
end
|
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.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-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|