gitlab_quality-test_tooling 3.5.0 → 3.6.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: 0afeda440d33c3cb9e1e81adf43ce462d12b604cffdf96b04dbe83a9a4d1f853
4
- data.tar.gz: 1a638b0a0b409ed28cb94da821a59f73967b6ae55cad7e87552a096b9c5c525e
3
+ metadata.gz: 4a734c368f480307b04c5cb1fb3e91059e0a8c357da88eb10e42b06071b7a528
4
+ data.tar.gz: 032f619282b68ef105086a34e1a67cb29bf9e620e3031d30fe941ddb9e36abc8
5
5
  SHA512:
6
- metadata.gz: 5bece6881a2ab2506f4d22b7da49b976af027a0f7d93c276d704f37fb52091ddde319232db0ccca1b717141267bbd5e283f30796a8bc06b14dbbbd38115327ca
7
- data.tar.gz: 7136b103704d811fcdaee9a44ee815775d2eb0411934654131d6478a8d2b62c3225a94a5602de169284cd3121a1b464bcbcf65570a5f7f923add0cdb0b5b386a
6
+ metadata.gz: 409cff4f9f3afd710b9b8e06d15e8b90cc550d59d15237ce8cbd11b813cdee938bba0873cbefcca8df55beb817c5a0fa4507d880d7124e520e74e549c48fdbe6
7
+ data.tar.gz: 9d1d9bcee36c44006824cee0f9cfafe80c1a8da902975dc6f53d9c7c64286775513b7e1830dee811487c9784a6223b31d853e3252a20488ceb98e012c8e4c8b7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.5.0)
4
+ gitlab_quality-test_tooling (3.6.0)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
data/README.md CHANGED
@@ -276,6 +276,53 @@ Usage: exe/feature-readiness-evaluation [options]
276
276
  -h, --help Show the usage
277
277
  ```
278
278
 
279
+ ### `exe/test-coverage`
280
+
281
+ ```shell
282
+ Purpose: Export test coverage metrics to ClickHouse
283
+ Usage: exe/test-coverage [options]
284
+
285
+ Options:
286
+ --test-reports GLOB Glob pattern for test JSON reports (RSpec or Jest) (e.g., "reports/**/*.json")
287
+ --coverage-report PATH Path to the LCOV coverage report (e.g., "coverage/lcov/gitlab.lcov")
288
+ --test-map PATH Path to the test map file (e.g., "crystalball/packed-mapping.json.gz")
289
+ --clickhouse-url URL ClickHouse server URL
290
+ --clickhouse-database DATABASE
291
+ ClickHouse database name
292
+ --clickhouse-username USERNAME
293
+ ClickHouse username
294
+ --clickhouse-shared-database DATABASE
295
+ ClickHouse shared database name (default: shared)
296
+ --responsibility-patterns PATH
297
+ Path to YAML file with responsibility classification patterns
298
+
299
+ Environment variables:
300
+ GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)
301
+
302
+ -h, --help Show the usage
303
+ -v, --version Show the version
304
+ ```
305
+
306
+ ### `exe/sync-category-owners`
307
+
308
+ ```shell
309
+ Purpose: Sync feature category ownership data from stages.yml to ClickHouse
310
+ Usage: exe/sync-category-owners [options]
311
+
312
+ Options:
313
+ --clickhouse-url URL ClickHouse server URL
314
+ --clickhouse-database DATABASE
315
+ ClickHouse database name (default: shared)
316
+ --clickhouse-username USERNAME
317
+ ClickHouse username
318
+
319
+ Environment variables:
320
+ GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required)
321
+
322
+ -h, --help Show the usage
323
+ -v, --version Show the version
324
+ ```
325
+
279
326
  ## Development
280
327
 
281
328
  ### Initial setup
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "uri"
6
+
7
+ require_relative "../lib/gitlab_quality/test_tooling"
8
+
9
+ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owners'
10
+ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
11
+
12
+ params = {}
13
+ required_params = [:clickhouse_url, :clickhouse_database, :clickhouse_username]
14
+
15
+ options = OptionParser.new do |opts|
16
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
17
+
18
+ opts.separator ""
19
+ opts.separator "Syncs feature category ownership data from stages.yml to ClickHouse."
20
+ opts.separator ""
21
+ opts.separator "Options:"
22
+
23
+ opts.on('--clickhouse-url URL', 'ClickHouse server URL') do |url|
24
+ params[:clickhouse_url] = url
25
+ end
26
+
27
+ opts.on('--clickhouse-database DATABASE', 'ClickHouse database name (default: shared)') do |database|
28
+ params[:clickhouse_database] = database
29
+ end
30
+
31
+ opts.on('--clickhouse-username USERNAME', 'ClickHouse username') do |username|
32
+ params[:clickhouse_username] = username
33
+ end
34
+
35
+ opts.separator ""
36
+ opts.separator "Environment variables:"
37
+ opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required)"
38
+ opts.separator ""
39
+
40
+ opts.on('-h', '--help', 'Show the usage') do
41
+ puts opts
42
+ exit
43
+ end
44
+
45
+ opts.on_tail('-v', '--version', 'Show the version') do
46
+ require_relative "../lib/gitlab_quality/test_tooling/version"
47
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
48
+ exit
49
+ end
50
+
51
+ opts.parse(ARGV)
52
+ end
53
+
54
+ # Default database to 'shared' if not specified
55
+ params[:clickhouse_database] ||= 'shared'
56
+
57
+ if params.any? && (required_params - params.keys).none?
58
+ clickhouse_password = ENV.fetch('GLCI_CLICKHOUSE_METRICS_PASSWORD', nil)
59
+ if clickhouse_password.to_s.strip.empty?
60
+ puts "Error: GLCI_CLICKHOUSE_METRICS_PASSWORD environment variable must be set and not empty"
61
+ exit 1
62
+ end
63
+
64
+ [:clickhouse_url, :clickhouse_database, :clickhouse_username].each do |param|
65
+ if params[param].to_s.strip.empty?
66
+ puts "Error: --#{param.to_s.tr('_', '-')} cannot be empty"
67
+ exit 1
68
+ end
69
+ end
70
+
71
+ begin
72
+ uri = URI.parse(params[:clickhouse_url])
73
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
74
+ puts "Error: --clickhouse-url must be a valid HTTP or HTTPS URL"
75
+ exit 1
76
+ end
77
+ rescue URI::InvalidURIError
78
+ puts "Error: --clickhouse-url is not a valid URL format"
79
+ exit 1
80
+ end
81
+
82
+ category_owners = GitlabQuality::TestTooling::CodeCoverage::CategoryOwners.new
83
+
84
+ category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(
85
+ url: params[:clickhouse_url],
86
+ database: params[:clickhouse_database],
87
+ username: params[:clickhouse_username],
88
+ password: clickhouse_password
89
+ )
90
+
91
+ category_owners_table.truncate
92
+ category_owners_table.push(category_owners.as_db_table)
93
+
94
+ puts "Successfully synced #{category_owners.as_db_table.length} feature categories to ClickHouse"
95
+ else
96
+ puts "Missing argument(s). Required arguments are: #{required_params.map { |p| "--#{p.to_s.tr('_', '-')}" }.join(', ')}"
97
+ puts options
98
+ exit 1
99
+ end
data/exe/test-coverage CHANGED
@@ -178,17 +178,18 @@ if params.any? && (required_params - params.keys).none?
178
178
  }
179
179
 
180
180
  category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**shared_clickhouse_data)
181
- coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
182
-
183
- if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
184
- category_owners_table.truncate
185
- category_owners_table.push(category_owners.as_db_table)
186
- end
187
-
181
+ coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(
182
+ category_owners_table: category_owners_table,
183
+ **clickhouse_data
184
+ )
188
185
  coverage_metrics_table.push(coverage_data.as_db_table)
189
186
 
190
187
  # Export test-to-file mappings
191
- test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(test_to_sources)
188
+ test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(
189
+ test_to_sources,
190
+ tests_to_categories: tests_to_categories,
191
+ feature_categories_to_teams: category_owners.feature_categories_to_teams
192
+ )
192
193
  test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
193
194
  test_file_mappings_table.push(test_file_mapping_data.as_db_table)
194
195
  else
@@ -116,14 +116,18 @@ module GitlabQuality
116
116
  groups = stage_data['groups'] || {}
117
117
  next unless section
118
118
 
119
- groups.each { |group, group_data| add_hierarchy_entry(section, stage, group, group_data['categories']) }
119
+ groups.each do |group, group_data|
120
+ add_hierarchy_entry(section, stage, group, group_data['categories'])
121
+ add_hierarchy_entry(section, stage, group, group_data['maintained_categories'])
122
+ end
120
123
  end
121
124
  end
122
125
 
123
126
  def add_hierarchy_entry(section, stage, group, categories)
124
127
  @hierarchy[section] ||= {}
125
128
  @hierarchy[section][stage] ||= {}
126
- @hierarchy[section][stage][group] = categories || []
129
+ @hierarchy[section][stage][group] ||= []
130
+ @hierarchy[section][stage][group].concat(Array(categories))
127
131
  end
128
132
 
129
133
  def populate_feature_categories_map(data, current_section = nil, current_stage = nil, current_group = nil)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'table'
4
+ require_relative 'category_owners_table'
4
5
 
5
6
  module GitlabQuality
6
7
  module TestTooling
@@ -9,8 +10,15 @@ module GitlabQuality
9
10
  class CoverageMetricsTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
10
11
  TABLE_NAME = "coverage_metrics"
11
12
 
13
+ def initialize(category_owners_table: nil, **args)
14
+ super(**args)
15
+ @category_owners_table = category_owners_table
16
+ end
17
+
12
18
  private
13
19
 
20
+ attr_reader :category_owners_table
21
+
14
22
  # @return [Boolean] True if the record is valid, false otherwise
15
23
  def valid_record?(record)
16
24
  valid_file?(record) &&
@@ -68,10 +76,47 @@ module GitlabQuality
68
76
  is_responsible: record[:is_responsible],
69
77
  is_dependent: record[:is_dependent],
70
78
  category: record[:feature_category],
79
+ **coverage_counts(record),
80
+ **org_data(record[:feature_category]),
71
81
  **ci_metadata
72
82
  }
73
83
  end
74
84
 
85
+ # @return [Hash] Raw coverage counts from the record
86
+ def coverage_counts(record)
87
+ {
88
+ total_lines: record[:total_lines] || 0,
89
+ covered_lines: record[:covered_lines] || 0,
90
+ total_branches: record[:total_branches] || 0,
91
+ covered_branches: record[:covered_branches] || 0,
92
+ total_functions: record[:total_functions] || 0,
93
+ covered_functions: record[:covered_functions] || 0
94
+ }
95
+ end
96
+
97
+ # @param category [String, nil] Feature category name
98
+ # @return [Hash] Organization data (group, stage, section) for the category
99
+ def org_data(category)
100
+ return { group: '', stage: '', section: '' } if category.nil? || category_owners_table.nil?
101
+
102
+ @org_data_cache ||= {}
103
+ @org_data_cache[category] ||= fetch_org_data(category)
104
+ end
105
+
106
+ # @param category [String] Feature category name
107
+ # @return [Hash] Organization data fetched from category_owners_table
108
+ def fetch_org_data(category)
109
+ owners = category_owners_table.owners(category)
110
+ {
111
+ group: owners['group'] || '',
112
+ stage: owners['stage'] || '',
113
+ section: owners['section'] || ''
114
+ }
115
+ rescue CategoryOwnersTable::MissingMappingError
116
+ logger.warn("#{LOG_PREFIX} No org data found for category '#{category}', using empty values")
117
+ { group: '', stage: '', section: '' }
118
+ end
119
+
75
120
  # @return [Hash] CI-related metadata
76
121
  def ci_metadata
77
122
  {
@@ -38,7 +38,11 @@ module GitlabQuality
38
38
  timestamp: time,
39
39
  test_file: record[:test_file],
40
40
  source_file: record[:source_file],
41
- ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil)
41
+ ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil),
42
+ category: record[:category] || '',
43
+ group: record[:group] || '',
44
+ stage: record[:stage] || '',
45
+ section: record[:section] || ''
42
46
  }
43
47
  end
44
48
  end
@@ -80,7 +80,13 @@ module GitlabQuality
80
80
  line_coverage: coverage_data&.dig(:percentage),
81
81
  branch_coverage: coverage_data&.dig(:branch_percentage),
82
82
  function_coverage: coverage_data&.dig(:function_percentage),
83
- source_file_type: @source_file_types[file] || 'other'
83
+ source_file_type: @source_file_types[file] || 'other',
84
+ total_lines: coverage_data&.dig(:total_lines) || 0,
85
+ covered_lines: coverage_data&.dig(:covered_lines) || 0,
86
+ total_branches: coverage_data&.dig(:total_branches) || 0,
87
+ covered_branches: coverage_data&.dig(:covered_branches) || 0,
88
+ total_functions: coverage_data&.dig(:total_functions) || 0,
89
+ covered_functions: coverage_data&.dig(:covered_functions) || 0
84
90
  }
85
91
  end
86
92
 
@@ -6,23 +6,36 @@ module GitlabQuality
6
6
  class TestFileMappingData
7
7
  # @param [Hash<String, Array<String>>] test_to_sources Test files
8
8
  # mapped to all source files they cover
9
- def initialize(test_to_sources)
9
+ # @param [Hash<String, Array<String>>] tests_to_categories Test files
10
+ # mapped to their feature categories
11
+ # @param [Hash<String, Hash>] feature_categories_to_teams Feature categories
12
+ # mapped to their org hierarchy (group, stage, section)
13
+ def initialize(test_to_sources, tests_to_categories: {}, feature_categories_to_teams: {})
10
14
  @test_to_sources = test_to_sources
15
+ @tests_to_categories = tests_to_categories
16
+ @feature_categories_to_teams = feature_categories_to_teams
11
17
  end
12
18
 
13
19
  # @return [Array<Hash<Symbol, String>>] Mapping data formatted for database insertion
14
20
  # @example Return value
15
21
  # [
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" },
22
+ # { test_file: "spec/models/user_spec.rb", source_file: "app/models/user.rb",
23
+ # category: "team_planning", group: "project_management", stage: "plan", section: "dev" },
18
24
  # ...
19
25
  # ]
20
26
  def as_db_table
21
27
  @test_to_sources.flat_map do |test_file, source_files|
28
+ category = @tests_to_categories[test_file]&.first || ''
29
+ team = @feature_categories_to_teams[category] || {}
30
+
22
31
  source_files.map do |source_file|
23
32
  {
24
33
  test_file: test_file,
25
- source_file: source_file
34
+ source_file: source_file,
35
+ category: category,
36
+ group: team[:group] || '',
37
+ stage: team[:stage] || '',
38
+ section: team[:section] || ''
26
39
  }
27
40
  end
28
41
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.5.0"
5
+ VERSION = "3.6.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.5.0
4
+ version: 3.6.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-01-23 00:00:00.000000000 Z
11
+ date: 2026-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -427,6 +427,7 @@ executables:
427
427
  - report-results
428
428
  - slow-test-issues
429
429
  - slow-test-merge-request-report-note
430
+ - sync-category-owners
430
431
  - test-coverage
431
432
  - update-screenshot-paths
432
433
  - update-test-meta
@@ -460,6 +461,7 @@ files:
460
461
  - exe/report-results
461
462
  - exe/slow-test-issues
462
463
  - exe/slow-test-merge-request-report-note
464
+ - exe/sync-category-owners
463
465
  - exe/test-coverage
464
466
  - exe/update-screenshot-paths
465
467
  - exe/update-test-meta