gitlab_quality-test_tooling 3.2.1 → 3.3.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 +7 -3
- data/lib/gitlab_quality/test_tooling/code_coverage/README.md +10 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +0 -23
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +0 -42
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +17 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +48 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +11 -1
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +33 -0
- 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: 32b52c2177aaf57f1ccf6a2bb922fd47e267b251ce74921c356f9631252051da
|
|
4
|
+
data.tar.gz: 8930c0fb427f3a946cd99902a230bcf90c48a051c9ffa1d19fb10a487107d231
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f265a7b7c74d4ef36e06c53a945b96ab066a4304acd0e6c439cf10afee6d0483de9253ca9290b279db7ec3c1d2323146da3aea8cf92aa57d4d1914c9c1479b1f
|
|
7
|
+
data.tar.gz: b983b542d66e9ac5b5775dd97b80ce6b464a618a5adf573f18613014dcfecc86555fc8b7d20e32b33b41b23ce9be255811cc276a50a1ee7945f0e4062deb15cf
|
data/Gemfile.lock
CHANGED
data/exe/test-coverage
CHANGED
|
@@ -10,11 +10,13 @@ require_relative "../lib/gitlab_quality/test_tooling"
|
|
|
10
10
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owners'
|
|
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
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table'
|
|
13
14
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data'
|
|
14
15
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
15
16
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
16
17
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
|
|
17
18
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
|
|
19
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data'
|
|
18
20
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
|
|
19
21
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier'
|
|
20
22
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config'
|
|
@@ -167,15 +169,17 @@ if params.any? && (required_params - params.keys).none?
|
|
|
167
169
|
category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
|
|
168
170
|
coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
|
|
169
171
|
|
|
170
|
-
category_owners_table.create if ENV['CLICKHOUSE_CREATE_CATEGORY_OWNERS_TABLE'] == 'true'
|
|
171
|
-
coverage_metrics_table.create if ENV['CLICKHOUSE_CREATE_COVERAGE_METRICS_TABLE'] == 'true'
|
|
172
|
-
|
|
173
172
|
if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
|
|
174
173
|
category_owners_table.truncate
|
|
175
174
|
category_owners_table.push(category_owners.as_db_table)
|
|
176
175
|
end
|
|
177
176
|
|
|
178
177
|
coverage_metrics_table.push(coverage_data.as_db_table)
|
|
178
|
+
|
|
179
|
+
# Export test-to-file mappings
|
|
180
|
+
test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(test_to_sources)
|
|
181
|
+
test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**clickhouse_data)
|
|
182
|
+
test_file_mappings_table.push(test_file_mapping_data.as_db_table)
|
|
179
183
|
else
|
|
180
184
|
puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
|
|
181
185
|
puts options
|
|
@@ -4,6 +4,7 @@ Exports test coverage data to ClickHouse, enriched with:
|
|
|
4
4
|
|
|
5
5
|
- **Feature category ownership** - group, stage, and section for each covered file
|
|
6
6
|
- **Responsibility classification** - whether coverage comes from unit or integration tests
|
|
7
|
+
- **Test-to-file mappings** - which source files each test covers
|
|
7
8
|
|
|
8
9
|
## How It Works
|
|
9
10
|
|
|
@@ -134,6 +135,15 @@ dependent:
|
|
|
134
135
|
- "^spec/features/"
|
|
135
136
|
```
|
|
136
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
|
+
|
|
137
147
|
## CLI
|
|
138
148
|
|
|
139
149
|
Example usage:
|
|
@@ -11,29 +11,6 @@ module GitlabQuality
|
|
|
11
11
|
|
|
12
12
|
MissingMappingError = Class.new(StandardError)
|
|
13
13
|
|
|
14
|
-
# Creates the ClickHouse table, if it doesn't exist already
|
|
15
|
-
# @return [nil]
|
|
16
|
-
def create
|
|
17
|
-
logger.debug("#{LOG_PREFIX} Creating category_owners table if it doesn't exist ...")
|
|
18
|
-
|
|
19
|
-
client.query(<<~SQL)
|
|
20
|
-
CREATE TABLE IF NOT EXISTS #{table_name} (
|
|
21
|
-
timestamp DateTime64(6, 'UTC') DEFAULT now64(),
|
|
22
|
-
category String,
|
|
23
|
-
group String,
|
|
24
|
-
stage String,
|
|
25
|
-
section String,
|
|
26
|
-
INDEX idx_group group TYPE set(360) GRANULARITY 1,
|
|
27
|
-
INDEX idx_stage stage TYPE set(360) GRANULARITY 1,
|
|
28
|
-
INDEX idx_section section TYPE set(360) GRANULARITY 1
|
|
29
|
-
) ENGINE = MergeTree()
|
|
30
|
-
ORDER BY (category, timestamp)
|
|
31
|
-
SETTINGS index_granularity = 8192;
|
|
32
|
-
SQL
|
|
33
|
-
|
|
34
|
-
logger.info("#{LOG_PREFIX} Category owners table created/verified successfully")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
14
|
def truncate
|
|
38
15
|
logger.debug("#{LOG_PREFIX} Truncating table #{full_table_name} ...")
|
|
39
16
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'time'
|
|
4
3
|
require_relative 'table'
|
|
5
4
|
|
|
6
5
|
module GitlabQuality
|
|
@@ -10,39 +9,6 @@ module GitlabQuality
|
|
|
10
9
|
class CoverageMetricsTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
|
|
11
10
|
TABLE_NAME = "coverage_metrics"
|
|
12
11
|
|
|
13
|
-
# Creates the ClickHouse table, if it doesn't exist already
|
|
14
|
-
# @return [nil]
|
|
15
|
-
def create
|
|
16
|
-
logger.debug("#{LOG_PREFIX} Creating coverage_metrics table if it doesn't exist ...")
|
|
17
|
-
|
|
18
|
-
client.query(<<~SQL)
|
|
19
|
-
CREATE TABLE IF NOT EXISTS #{table_name} (
|
|
20
|
-
timestamp DateTime64(6, 'UTC'),
|
|
21
|
-
file String,
|
|
22
|
-
line_coverage Float64,
|
|
23
|
-
branch_coverage Nullable(Float64),
|
|
24
|
-
function_coverage Nullable(Float64),
|
|
25
|
-
source_file_type String,
|
|
26
|
-
is_responsible Nullable(Bool),
|
|
27
|
-
is_dependent Nullable(Bool),
|
|
28
|
-
category Nullable(String),
|
|
29
|
-
ci_project_id Nullable(UInt32),
|
|
30
|
-
ci_project_path Nullable(String),
|
|
31
|
-
ci_job_name Nullable(String),
|
|
32
|
-
ci_job_id Nullable(UInt64),
|
|
33
|
-
ci_pipeline_id Nullable(UInt64),
|
|
34
|
-
ci_merge_request_iid Nullable(UInt32),
|
|
35
|
-
ci_branch Nullable(String),
|
|
36
|
-
ci_target_branch Nullable(String)
|
|
37
|
-
) ENGINE = MergeTree()
|
|
38
|
-
PARTITION BY toYYYYMM(timestamp)
|
|
39
|
-
ORDER BY (ci_project_path, timestamp, file, ci_pipeline_id)
|
|
40
|
-
SETTINGS index_granularity = 8192, allow_nullable_key = 1;
|
|
41
|
-
SQL
|
|
42
|
-
|
|
43
|
-
logger.info("#{LOG_PREFIX} Coverage metrics table created/verified successfully")
|
|
44
|
-
end
|
|
45
|
-
|
|
46
12
|
private
|
|
47
13
|
|
|
48
14
|
# @return [Boolean] True if the record is valid, false otherwise
|
|
@@ -106,14 +72,6 @@ module GitlabQuality
|
|
|
106
72
|
}
|
|
107
73
|
end
|
|
108
74
|
|
|
109
|
-
# @return [Time] Common timestamp for all coverage records
|
|
110
|
-
def time
|
|
111
|
-
@time ||= begin
|
|
112
|
-
ci_created_at = ENV.fetch('CI_PIPELINE_CREATED_AT', nil)
|
|
113
|
-
ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
75
|
# @return [Hash] CI-related metadata
|
|
118
76
|
def ci_metadata
|
|
119
77
|
{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
3
5
|
module GitlabQuality
|
|
4
6
|
module TestTooling
|
|
5
7
|
module CodeCoverage
|
|
@@ -58,6 +60,21 @@ module GitlabQuality
|
|
|
58
60
|
raise NotImplementedError, "#{self.class}##{__method__} method must be implemented in a subclass"
|
|
59
61
|
end
|
|
60
62
|
|
|
63
|
+
# @return [Time] Common timestamp for all records, memoized
|
|
64
|
+
def time
|
|
65
|
+
@time ||= parse_ci_timestamp
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_ci_timestamp
|
|
69
|
+
ci_created_at = ENV.fetch('CI_PIPELINE_CREATED_AT', nil)
|
|
70
|
+
return Time.now.utc unless ci_created_at
|
|
71
|
+
|
|
72
|
+
Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z')
|
|
73
|
+
rescue ArgumentError
|
|
74
|
+
logger.warn("#{LOG_PREFIX} Invalid CI_PIPELINE_CREATED_AT format: #{ci_created_at}, using current time")
|
|
75
|
+
Time.now.utc
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
# @return [GitlabQuality::TestTooling::ClickHouse::Client]
|
|
62
79
|
def client
|
|
63
80
|
@client ||= GitlabQuality::TestTooling::ClickHouse::Client.new(
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
private
|
|
13
|
+
|
|
14
|
+
# @return [Boolean] True if the record is valid, false otherwise
|
|
15
|
+
def valid_record?(record)
|
|
16
|
+
valid_test_file?(record) && valid_source_file?(record)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Boolean] True if the test_file field is present
|
|
20
|
+
def valid_test_file?(record)
|
|
21
|
+
return true unless record[:test_file].blank?
|
|
22
|
+
|
|
23
|
+
logger.warn("#{LOG_PREFIX} Skipping record with nil/empty test_file: #{record}")
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] True if the source_file field is present
|
|
28
|
+
def valid_source_file?(record)
|
|
29
|
+
return true unless record[:source_file].blank?
|
|
30
|
+
|
|
31
|
+
logger.warn("#{LOG_PREFIX} Skipping record with nil/empty source_file: #{record}")
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Hash] Transformed mapping data including timestamp and CI metadata
|
|
36
|
+
def sanitized_data_record(record)
|
|
37
|
+
{
|
|
38
|
+
timestamp: time,
|
|
39
|
+
test_file: record[:test_file],
|
|
40
|
+
source_file: record[:source_file],
|
|
41
|
+
ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -85,7 +85,7 @@ module GitlabQuality
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def register_source_file(filename)
|
|
88
|
-
@current_file = filename
|
|
88
|
+
@current_file = normalize_path(filename)
|
|
89
89
|
@parsed_content[@current_file] = {
|
|
90
90
|
line_coverage: {},
|
|
91
91
|
branch_coverage: {},
|
|
@@ -94,6 +94,16 @@ module GitlabQuality
|
|
|
94
94
|
}
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
def normalize_path(filename)
|
|
98
|
+
# Remove leading ./ if present
|
|
99
|
+
path = filename.gsub(%r{^\./}, '')
|
|
100
|
+
|
|
101
|
+
# Handle GDK/CI paths like "../../../home/gdk/gitlab-development-kit/gitlab/app/..."
|
|
102
|
+
# Extract path starting from known root directories
|
|
103
|
+
match = path.match(%r{((?:ee/)?(?:app|lib|config|db|spec|scripts|tooling|workhorse|vendor)/.+)$})
|
|
104
|
+
match ? match[1] : path
|
|
105
|
+
end
|
|
106
|
+
|
|
97
107
|
def register_line_data(line_no, count)
|
|
98
108
|
return unless @current_file
|
|
99
109
|
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
def initialize(test_to_sources)
|
|
10
|
+
@test_to_sources = test_to_sources
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [Array<Hash<Symbol, String>>] Mapping data formatted for database insertion
|
|
14
|
+
# @example Return value
|
|
15
|
+
# [
|
|
16
|
+
# { test_file: "spec/models/user_spec.rb", source_file: "app/models/user.rb" },
|
|
17
|
+
# { test_file: "spec/models/user_spec.rb", source_file: "lib/utils.rb" },
|
|
18
|
+
# ...
|
|
19
|
+
# ]
|
|
20
|
+
def as_db_table
|
|
21
|
+
@test_to_sources.flat_map do |test_file, source_files|
|
|
22
|
+
source_files.map do |source_file|
|
|
23
|
+
{
|
|
24
|
+
test_file: test_file,
|
|
25
|
+
source_file: source_file
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
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.3.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: 2025-12-
|
|
11
|
+
date: 2025-12-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -490,12 +490,14 @@ files:
|
|
|
490
490
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb
|
|
491
491
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb
|
|
492
492
|
- lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb
|
|
493
|
+
- lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb
|
|
493
494
|
- lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb
|
|
494
495
|
- lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
|
|
495
496
|
- lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb
|
|
496
497
|
- lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb
|
|
497
498
|
- lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
|
|
498
499
|
- lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb
|
|
500
|
+
- lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb
|
|
499
501
|
- lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
|
|
500
502
|
- lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
|
|
501
503
|
- lib/gitlab_quality/test_tooling/code_coverage/utils.rb
|