gitlab_quality-test_tooling 3.23.0 → 3.23.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/README.md +1 -1
- data/exe/post-to-slack +4 -3
- data/exe/test-coverage +0 -10
- data/lib/gitlab_quality/test_tooling/code_coverage/README.md +0 -9
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +2 -4
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +0 -263
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49a063bdf7dc0b0055d926d178df7a5274c067f352a5feae0d5f04695702ea01
|
|
4
|
+
data.tar.gz: 426701ca7b4f8b55c433bfd78b173f1e731c1f55dafd099584acb0366b6f77de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3a5eb95bc469a879dacb6db1e3a456b35373391ed034df6a4adec4840533666613367de04510f4a8b9e1c60f755bfc5140b6a32048a597ec822bfc1c10cfd92
|
|
7
|
+
data.tar.gz: 00136e319209d433a0ff36c7c4a583ea54848653a5b1290244ba8d9108e47c0e3f7aad4a736cd7f3b51d42afe8d3beb16074c655cb41939f0135e555cfd96efb
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -248,7 +248,7 @@ Usage: exe/feature-readiness-evaluation [options]
|
|
|
248
248
|
|
|
249
249
|
Runs in one of three modes:
|
|
250
250
|
|
|
251
|
-
- **Full coverage-metrics export** (default, no `--per-test-coverage` / `--run-aggregation`): reads the LCOV report, Crystalball test map, and test reports, then exports the coverage-metrics
|
|
251
|
+
- **Full coverage-metrics export** (default, no `--per-test-coverage` / `--run-aggregation`): reads the LCOV report, Crystalball test map, and test reports, then exports the coverage-metrics table. Requires the full argument set.
|
|
252
252
|
- **Per-test coverage** (`--per-test-coverage GLOB`): inserts per-test rows into `code_coverage.test_coverage_per_file` and runs the daily `test_health_risk` aggregation unless `--skip-aggregation`. Needs only the `--clickhouse-*` options; `--test-reports` and `--jest-quarantine-file` are optional enrichment.
|
|
253
253
|
- **Aggregation only** (`--run-aggregation`): runs the daily `test_health_risk` aggregation and exits. Needs only the `--clickhouse-*` options. Pair with `--skip-aggregation` on batched per-test inserts to aggregate once at the end instead of once per batch.
|
|
254
254
|
|
data/exe/post-to-slack
CHANGED
|
@@ -15,11 +15,12 @@ gitlab_api_token = nil
|
|
|
15
15
|
options = OptionParser.new do |opts|
|
|
16
16
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# No String type on the webhook URL and bot token: OptionParser's String rejects "",
|
|
19
|
+
# and CI passes an unset variable as "".
|
|
20
|
+
opts.on('-w', '--slack-webhook-url SLACK_WEBHOOK_URL', 'Slack webhook URL') do |slack_webhook_url|
|
|
21
|
+
params[:slack_webhook_url] = slack_webhook_url unless slack_webhook_url.empty?
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
# No String type: OptionParser's String rejects "", and CI passes an unset variable as "".
|
|
23
24
|
opts.on('-b', '--slack-bot-token TOKEN', 'Slack bot token, used instead of the webhook URL when set') do |token|
|
|
24
25
|
params[:slack_bot_token] = token unless token.empty?
|
|
25
26
|
end
|
data/exe/test-coverage
CHANGED
|
@@ -11,7 +11,6 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owne
|
|
|
11
11
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
|
|
12
12
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table'
|
|
13
13
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/per_test_coverage_table'
|
|
14
|
-
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table'
|
|
15
14
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/jest_quarantined_tests_table'
|
|
16
15
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator'
|
|
17
16
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter'
|
|
@@ -20,7 +19,6 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
|
20
19
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
21
20
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
|
|
22
21
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
|
|
23
|
-
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data'
|
|
24
22
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_data'
|
|
25
23
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_exporter'
|
|
26
24
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
|
|
@@ -279,12 +277,4 @@ when :full_export
|
|
|
279
277
|
**clickhouse_data
|
|
280
278
|
)
|
|
281
279
|
coverage_metrics_table.push(coverage_data.as_db_table)
|
|
282
|
-
|
|
283
|
-
# Export test-to-file mappings
|
|
284
|
-
test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(
|
|
285
|
-
test_to_sources,
|
|
286
|
-
tests_to_categories: tests_to_categories
|
|
287
|
-
)
|
|
288
|
-
test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
|
|
289
|
-
test_file_mappings_table.push(test_file_mapping_data.as_db_table)
|
|
290
280
|
end
|
|
@@ -135,15 +135,6 @@ dependent:
|
|
|
135
135
|
- "^spec/features/"
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
## Test-to-File Mappings
|
|
139
|
-
|
|
140
|
-
When a test map is provided, the module also exports test-to-source-file relationships
|
|
141
|
-
to a separate `test_file_mappings` table. This enables:
|
|
142
|
-
|
|
143
|
-
- **Coverage context for tests** - see which source files a specific test covers
|
|
144
|
-
- **Impact analysis** - understand which files would lose coverage if a test is quarantined
|
|
145
|
-
- **Flaky test triage** - correlate flaky tests with the source files they cover
|
|
146
|
-
|
|
147
138
|
## CLI
|
|
148
139
|
|
|
149
140
|
Example usage:
|
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.23.
|
|
4
|
+
version: 3.23.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-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -502,7 +502,6 @@ files:
|
|
|
502
502
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/jest_quarantined_tests_table.rb
|
|
503
503
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/per_test_coverage_table.rb
|
|
504
504
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb
|
|
505
|
-
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb
|
|
506
505
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql
|
|
507
506
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator.rb
|
|
508
507
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshot.sql
|
|
@@ -515,7 +514,6 @@ files:
|
|
|
515
514
|
- lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb
|
|
516
515
|
- lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
|
|
517
516
|
- lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb
|
|
518
|
-
- lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb
|
|
519
517
|
- lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
|
|
520
518
|
- lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
|
|
521
519
|
- lib/gitlab_quality/test_tooling/code_coverage/utils.rb
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'table'
|
|
4
|
-
|
|
5
|
-
module GitlabQuality
|
|
6
|
-
module TestTooling
|
|
7
|
-
module CodeCoverage
|
|
8
|
-
module ClickHouse
|
|
9
|
-
class TestFileMappingsTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
|
|
10
|
-
TABLE_NAME = "test_file_mappings"
|
|
11
|
-
|
|
12
|
-
# Override push to skip mappings already stored with the same values.
|
|
13
|
-
#
|
|
14
|
-
# @param data [Array<Hash>] Code coverage related data to be pushed to ClickHouse
|
|
15
|
-
# @return [nil]
|
|
16
|
-
def push(data) # rubocop:disable Metrics/AbcSize
|
|
17
|
-
return logger.warn("#{LOG_PREFIX} No data found, skipping ClickHouse export!") if data.empty?
|
|
18
|
-
|
|
19
|
-
logger.debug("#{LOG_PREFIX} Starting data export to ClickHouse")
|
|
20
|
-
@dropped_source_files = []
|
|
21
|
-
sanitized_data = sanitize(data)
|
|
22
|
-
report_dropped_source_files(sanitized_data.size)
|
|
23
|
-
|
|
24
|
-
return logger.warn("#{LOG_PREFIX} No valid data found after sanitization, skipping ClickHouse export!") if sanitized_data.empty?
|
|
25
|
-
|
|
26
|
-
# Filter out records that already exist with identical values
|
|
27
|
-
new_or_changed_records = filter_duplicates(sanitized_data)
|
|
28
|
-
|
|
29
|
-
if new_or_changed_records.empty?
|
|
30
|
-
logger.info("#{LOG_PREFIX} None of the #{sanitized_data.size} mappings need writing to #{full_table_name}, skipping insert")
|
|
31
|
-
return
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
client.insert_json_data(table_name, new_or_changed_records)
|
|
35
|
-
skipped_count = sanitized_data.size - new_or_changed_records.size
|
|
36
|
-
logger.info("#{LOG_PREFIX} Successfully pushed #{new_or_changed_records.size} new/changed records " \
|
|
37
|
-
"to #{full_table_name} (#{skipped_count} not written)")
|
|
38
|
-
rescue StandardError => e
|
|
39
|
-
logger.error("#{LOG_PREFIX} Error occurred while pushing data to #{full_table_name}: #{e.message}")
|
|
40
|
-
raise
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
# @return [Boolean] True if the record is valid, false otherwise
|
|
46
|
-
def valid_record?(record)
|
|
47
|
-
valid_test_file?(record) && valid_source_file?(record)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# @return [Boolean] True if the test_file field is present
|
|
51
|
-
def valid_test_file?(record)
|
|
52
|
-
return true unless record[:test_file].blank?
|
|
53
|
-
|
|
54
|
-
logger.warn("#{LOG_PREFIX} Skipping record with nil/empty test_file: #{record}")
|
|
55
|
-
false
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# @return [Boolean] True if the source_file field is present
|
|
59
|
-
def valid_source_file?(record)
|
|
60
|
-
return true unless record[:source_file].blank?
|
|
61
|
-
|
|
62
|
-
logger.warn("#{LOG_PREFIX} Skipping record with nil/empty source_file: #{record}")
|
|
63
|
-
false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# @return [Hash, nil] Transformed mapping data including timestamp and CI metadata,
|
|
67
|
-
# or nil when the source path cannot be made repo-relative
|
|
68
|
-
def sanitized_data_record(record)
|
|
69
|
-
source_file = relative_source_file(record[:source_file])
|
|
70
|
-
|
|
71
|
-
if source_file.nil?
|
|
72
|
-
dropped_source_files << record[:source_file]
|
|
73
|
-
return
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
timestamp: time,
|
|
78
|
-
test_file: record[:test_file],
|
|
79
|
-
source_file: source_file,
|
|
80
|
-
# CI_PROJECT_PATH is set by GitLab CI runner and considered trusted infrastructure input.
|
|
81
|
-
# In non-CI environments, this may be user-controlled, but sanitize_for_clickhouse
|
|
82
|
-
# provides SQL injection protection.
|
|
83
|
-
ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil),
|
|
84
|
-
category: record[:category] || ''
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def dropped_source_files
|
|
89
|
-
@dropped_source_files ||= []
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Reports the records that relative_source_file could not make repo-relative, once
|
|
93
|
-
# for the whole run, and raises when none survived. A CI_PROJECT_DIR that does not
|
|
94
|
-
# match the artifact's paths drops every mapping, so warning per record buries the
|
|
95
|
-
# job trace in one line per mapping, and warning alone would let a wholly empty
|
|
96
|
-
# export finish green.
|
|
97
|
-
#
|
|
98
|
-
# @return [void]
|
|
99
|
-
def report_dropped_source_files(kept_count)
|
|
100
|
-
return if dropped_source_files.empty?
|
|
101
|
-
|
|
102
|
-
message = "#{LOG_PREFIX} Skipped #{dropped_source_files.size} records with " \
|
|
103
|
-
"non-relative source_file, samples: #{dropped_source_files.first(3).join(', ')}"
|
|
104
|
-
|
|
105
|
-
raise message if kept_count.zero?
|
|
106
|
-
|
|
107
|
-
logger.warn(message)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# The repo-relative form of a source path, which is what the table's
|
|
111
|
-
# valid_source_file_format constraint accepts. Returns nil when the path cannot be
|
|
112
|
-
# made relative or normalizes to nothing, and the caller drops that record, because
|
|
113
|
-
# ClickHouse rejects the whole insert batch on a constraint violation rather than
|
|
114
|
-
# the offending row.
|
|
115
|
-
#
|
|
116
|
-
# @return [String, nil]
|
|
117
|
-
def relative_source_file(path)
|
|
118
|
-
ci_project_dir = ENV.fetch('CI_PROJECT_DIR', nil)
|
|
119
|
-
|
|
120
|
-
relative = path
|
|
121
|
-
relative = relative.delete_prefix("#{ci_project_dir.chomp('/')}/") if ci_project_dir.present?
|
|
122
|
-
relative = relative.sub(%r{\A(\./)+}, '')
|
|
123
|
-
|
|
124
|
-
relative.presence unless relative.start_with?('/')
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# The records worth writing: mappings this project has not stored yet, plus ones
|
|
128
|
-
# whose category changed, except that a blank never replaces a stored category.
|
|
129
|
-
# Re-sending an unchanged mapping costs a row that survives until the table's TTL
|
|
130
|
-
# expires it, because section and stage are still part of the sort key and the
|
|
131
|
-
# values written here no longer match the stored ones.
|
|
132
|
-
#
|
|
133
|
-
# If the stored categories cannot be read, see #insertable_without_lookup.
|
|
134
|
-
#
|
|
135
|
-
# @param records [Array<Hash>] Sanitized records to insert
|
|
136
|
-
# @return [Array<Hash>] Records that are new or have changed values
|
|
137
|
-
def filter_duplicates(records) # rubocop:disable Metrics/AbcSize
|
|
138
|
-
return records if records.empty?
|
|
139
|
-
|
|
140
|
-
# Assumes all records belong to the same project (batch from single pipeline run)
|
|
141
|
-
ci_project_path = records.first[:ci_project_path]
|
|
142
|
-
return records if ci_project_path.blank? # Can't query without project path
|
|
143
|
-
|
|
144
|
-
logger.debug("#{LOG_PREFIX} Checking for duplicate mappings in #{full_table_name}")
|
|
145
|
-
|
|
146
|
-
existing_mappings = fetch_existing_mappings(ci_project_path, records)
|
|
147
|
-
return insertable_without_lookup(records) if existing_mappings.nil?
|
|
148
|
-
|
|
149
|
-
blank_suppressed = 0
|
|
150
|
-
|
|
151
|
-
new_or_changed = records.select do |record|
|
|
152
|
-
key = "#{record[:test_file]}|#{record[:source_file]}"
|
|
153
|
-
existing = existing_mappings[key]
|
|
154
|
-
|
|
155
|
-
# New mapping - doesn't exist yet
|
|
156
|
-
next true if existing.nil?
|
|
157
|
-
|
|
158
|
-
# A blank category means this pipeline's rspec reports didn't cover the test,
|
|
159
|
-
# not that the test has no category: the mappings come from the Crystalball map,
|
|
160
|
-
# which lists every test file regardless of what ran. Writing the blank erases a
|
|
161
|
-
# known category, and readers resolving ownership through category get nothing.
|
|
162
|
-
if record[:category].blank? && existing[:category].present?
|
|
163
|
-
blank_suppressed += 1
|
|
164
|
-
next false
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
existing[:category] != record[:category]
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
log_blank_suppressions(blank_suppressed)
|
|
171
|
-
logger.debug("#{LOG_PREFIX} Found #{new_or_changed.size} new/changed mappings out of #{records.size} total")
|
|
172
|
-
new_or_changed
|
|
173
|
-
rescue StandardError => e
|
|
174
|
-
logger.warn("#{LOG_PREFIX} Failed to check for duplicates: #{e.message}")
|
|
175
|
-
insertable_without_lookup(records)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# What to write when the stored categories could not be read. Records carrying a
|
|
179
|
-
# category still go in, because a failed read must not silently drop mappings.
|
|
180
|
-
# Blanks are held back: with no stored value to compare against they look new, and
|
|
181
|
-
# writing one replaces whatever category is already there. The mapping comes back
|
|
182
|
-
# on the next export that can read.
|
|
183
|
-
#
|
|
184
|
-
# @param records [Array<Hash>] Sanitized records to insert
|
|
185
|
-
# @return [Array<Hash>] Records that carry a category
|
|
186
|
-
def insertable_without_lookup(records)
|
|
187
|
-
with_category = records.reject { |record| record[:category].blank? }
|
|
188
|
-
|
|
189
|
-
logger.warn("#{LOG_PREFIX} No duplicate check possible; inserting " \
|
|
190
|
-
"#{with_category.size} of #{records.size} mappings and holding back " \
|
|
191
|
-
"#{records.size - with_category.size} with no category")
|
|
192
|
-
|
|
193
|
-
with_category
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Reported at info because it is the only signal that a pipeline's reports were
|
|
197
|
-
# incomplete. A steady non-zero count across runs means categories are arriving
|
|
198
|
-
# from fewer reports than there are test files in the map.
|
|
199
|
-
#
|
|
200
|
-
# @param count [Integer] Mappings whose stored category was kept over an incoming blank
|
|
201
|
-
# @return [nil]
|
|
202
|
-
def log_blank_suppressions(count)
|
|
203
|
-
return unless count.positive?
|
|
204
|
-
|
|
205
|
-
logger.info("#{LOG_PREFIX} Kept the stored category for #{count} mappings " \
|
|
206
|
-
"this pipeline's reports did not cover")
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# The category currently stored against each of this project's mappings. Only
|
|
210
|
-
# filter_duplicates reads it.
|
|
211
|
-
#
|
|
212
|
-
# @param ci_project_path [String] The CI project path to filter by
|
|
213
|
-
# @param records [Array<Hash>] Records being inserted (used to limit query scope)
|
|
214
|
-
# @return [Hash, nil] "test_file|source_file" => { category: String }, or nil when
|
|
215
|
-
# the read fails. An empty Hash means the table holds nothing for this project.
|
|
216
|
-
def fetch_existing_mappings(ci_project_path, records) # rubocop:disable Metrics/AbcSize
|
|
217
|
-
# Query for ALL latest mappings for this project
|
|
218
|
-
# We filter in Ruby rather than building dynamic SQL to avoid injection risks
|
|
219
|
-
# Aggregate rather than matching rows against a set of the latest timestamps.
|
|
220
|
-
# The set form has to materialise one tuple per stored mapping before it can
|
|
221
|
-
# filter anything, which exceeds the server's memory limit at this table's
|
|
222
|
-
# size; argMax streams and peaks at roughly a quarter of it.
|
|
223
|
-
sql = <<~SQL
|
|
224
|
-
SELECT
|
|
225
|
-
test_file,
|
|
226
|
-
source_file,
|
|
227
|
-
argMax(category, timestamp) AS category
|
|
228
|
-
FROM #{full_table_name}
|
|
229
|
-
WHERE ci_project_path = {project_path:String}
|
|
230
|
-
GROUP BY test_file, source_file
|
|
231
|
-
SQL
|
|
232
|
-
|
|
233
|
-
# Substitute project_path value using manual escaping (client doesn't support parameterized queries)
|
|
234
|
-
sql_with_params = sql.strip.gsub('{project_path:String}', "'#{sanitize_for_clickhouse(ci_project_path)}'")
|
|
235
|
-
results = client.query(sql_with_params)
|
|
236
|
-
|
|
237
|
-
# Build lookup hash, filtering to only the test_files we're checking if dataset is small
|
|
238
|
-
test_files_set = records.to_set { |r| r[:test_file] } if records.size <= 10_000
|
|
239
|
-
|
|
240
|
-
results.each_with_object({}) do |row, hash|
|
|
241
|
-
# Skip if we're doing selective filtering and this test_file isn't in our set
|
|
242
|
-
next if test_files_set && !test_files_set.include?(row['test_file'])
|
|
243
|
-
|
|
244
|
-
key = "#{row['test_file']}|#{row['source_file']}"
|
|
245
|
-
hash[key] = { category: row['category'] }
|
|
246
|
-
end
|
|
247
|
-
rescue StandardError => e
|
|
248
|
-
logger.warn("#{LOG_PREFIX} Failed to fetch existing mappings: #{e.message}")
|
|
249
|
-
nil # nil, not {}: an empty table and an unreadable one need different handling
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Sanitize string for ClickHouse by escaping single quotes and backslashes
|
|
253
|
-
# This protects against SQL injection when we must use string interpolation
|
|
254
|
-
# @param str [String] String to sanitize
|
|
255
|
-
# @return [String] Sanitized string
|
|
256
|
-
def sanitize_for_clickhouse(str)
|
|
257
|
-
str.to_s.gsub(/\\/, '\\\\\\\\').gsub("'", "''") # rubocop:disable Style/RedundantRegexpArgument
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
end
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GitlabQuality
|
|
4
|
-
module TestTooling
|
|
5
|
-
module CodeCoverage
|
|
6
|
-
class TestFileMappingData
|
|
7
|
-
# @param [Hash<String, Array<String>>] test_to_sources Test files
|
|
8
|
-
# mapped to all source files they cover
|
|
9
|
-
# @param [Hash<String, Array<String>>] tests_to_categories Test files
|
|
10
|
-
# mapped to their feature categories
|
|
11
|
-
def initialize(test_to_sources, tests_to_categories: {})
|
|
12
|
-
@test_to_sources = test_to_sources
|
|
13
|
-
@tests_to_categories = tests_to_categories
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Rows for shared.test_file_mappings, one per test-and-source pair.
|
|
17
|
-
#
|
|
18
|
-
# Carries no group, stage or section on purpose. Those describe the category
|
|
19
|
-
# rather than the mapping and change whenever stages.yml is reorganised, so
|
|
20
|
-
# storing them on every mapping multiplies rows for no gain.
|
|
21
|
-
#
|
|
22
|
-
# Readers resolve ownership from shared.category_owners through category. That
|
|
23
|
-
# table keeps every historical row, up to 57 for one category, so a plain join
|
|
24
|
-
# fans out; dedupe it with ORDER BY category, timestamp DESC LIMIT 1 BY category.
|
|
25
|
-
#
|
|
26
|
-
# @return [Array<Hash<Symbol, String>>] Mapping data formatted for database insertion
|
|
27
|
-
# @example Return value
|
|
28
|
-
# [
|
|
29
|
-
# { test_file: "spec/models/user_spec.rb", source_file: "app/models/user.rb",
|
|
30
|
-
# category: "team_planning" },
|
|
31
|
-
# ...
|
|
32
|
-
# ]
|
|
33
|
-
def as_db_table
|
|
34
|
-
@test_to_sources.flat_map do |test_file, source_files|
|
|
35
|
-
category = @tests_to_categories[test_file]&.first || ''
|
|
36
|
-
|
|
37
|
-
source_files.map do |source_file|
|
|
38
|
-
{
|
|
39
|
-
test_file: test_file,
|
|
40
|
-
source_file: source_file,
|
|
41
|
-
category: category
|
|
42
|
-
}
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|