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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7463b8e510a9decd2156d3167e28d6109c99928b03cc5738be19eba7ecccfa39
4
- data.tar.gz: 2094146a3e6ebcbbcf17ede753e5b1c822b8c33b397cc02550b5c17dac1d19a6
3
+ metadata.gz: 32b52c2177aaf57f1ccf6a2bb922fd47e267b251ce74921c356f9631252051da
4
+ data.tar.gz: 8930c0fb427f3a946cd99902a230bcf90c48a051c9ffa1d19fb10a487107d231
5
5
  SHA512:
6
- metadata.gz: d1fe5d32ed4521023381d238ad1a5bd17d7b36ea32b9bd888697e0126452a36fab14d2884d3d665f55620cd76d7f9da5048b5bb18b9f61746b0b49e49ed291f5
7
- data.tar.gz: df998fa7999ae675f4de19227bef692594427c02c30116f9312fbcc07d73312bcccb634efabafe20cea3fc7fa947bb85feab19829b13b1071fd5e5626ed0c5a9
6
+ metadata.gz: f265a7b7c74d4ef36e06c53a945b96ab066a4304acd0e6c439cf10afee6d0483de9253ca9290b279db7ec3c1d2323146da3aea8cf92aa57d4d1914c9c1479b1f
7
+ data.tar.gz: b983b542d66e9ac5b5775dd97b80ce6b464a618a5adf573f18613014dcfecc86555fc8b7d20e32b33b41b23ce9be255811cc276a50a1ee7945f0e4062deb15cf
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.2.1)
4
+ gitlab_quality-test_tooling (3.3.0)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
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.gsub(%r{^\./}, '')
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.2.1"
5
+ VERSION = "3.3.0"
6
6
  end
7
7
  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.2.1
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-12 00:00:00.000000000 Z
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