gitlab_quality-test_tooling 3.0.0 → 3.5.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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -4
  3. data/README.md +0 -14
  4. data/exe/test-coverage +51 -8
  5. data/lib/gitlab_quality/test_tooling/code_coverage/README.md +162 -0
  6. data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +5 -2
  7. data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +26 -26
  8. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +13 -27
  9. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +3 -41
  10. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +17 -0
  11. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +48 -0
  12. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +71 -34
  13. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +11 -1
  14. data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb +47 -0
  15. data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb +46 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +33 -0
  17. data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +2 -4
  18. data/lib/gitlab_quality/test_tooling/runtime/env.rb +4 -0
  19. data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +1 -1
  20. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +4 -4
  21. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +8 -0
  22. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +36 -10
  23. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +2 -0
  24. data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_formatter.rb +38 -0
  25. data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_helper.rb +76 -0
  26. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +15 -1
  27. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  28. metadata +9 -28
  29. data/exe/existing-test-health-issue +0 -59
  30. data/exe/generate-test-session +0 -70
  31. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +0 -288
  32. data/lib/gitlab_quality/test_tooling/report/test_health_issue_finder.rb +0 -79
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f037a73b3fd4a445324a37084915ea2d020475cca6f4e3b1d23045f290351082
4
- data.tar.gz: e32bc045856df09004e725b9b84317c74ffb66b59ed640b078e292f46a3ff565
3
+ metadata.gz: 0afeda440d33c3cb9e1e81adf43ce462d12b604cffdf96b04dbe83a9a4d1f853
4
+ data.tar.gz: 1a638b0a0b409ed28cb94da821a59f73967b6ae55cad7e87552a096b9c5c525e
5
5
  SHA512:
6
- metadata.gz: 400c1e51bf57a3f10cdb36c083444153326da8710edd828839881867aed4d9282da4b21fa16357f49bb498ac48199dda888e422e1ddf4f5453769cb5bdc92211
7
- data.tar.gz: cc071d0ca3c54a0d3408f6fc91165348c403ab5109c473d7bc518701a17f90d992f02139614dfb99fd53cac7e0ffef688241c7c232cf373344269ffe3e8b4436
6
+ metadata.gz: 5bece6881a2ab2506f4d22b7da49b976af027a0f7d93c276d704f37fb52091ddde319232db0ccca1b717141267bbd5e283f30796a8bc06b14dbbbd38115327ca
7
+ data.tar.gz: 7136b103704d811fcdaee9a44ee815775d2eb0411934654131d6478a8d2b62c3225a94a5602de169284cd3121a1b464bcbcf65570a5f7f923add0cdb0b5b386a
data/Gemfile.lock CHANGED
@@ -1,13 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.0.0)
5
- activesupport (>= 7.0, < 7.3)
4
+ gitlab_quality-test_tooling (3.5.0)
5
+ activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
8
8
  gitlab (>= 4.19, < 7.0)
9
9
  http (~> 5.0)
10
- influxdb-client (~> 3.1)
11
10
  nokogiri (~> 1.10)
12
11
  parallel (>= 1, < 2)
13
12
  rainbow (>= 3, < 4)
@@ -200,7 +199,6 @@ GEM
200
199
  httpclient (2.8.3)
201
200
  i18n (1.14.6)
202
201
  concurrent-ruby (~> 1.0)
203
- influxdb-client (3.1.0)
204
202
  jaro_winkler (1.6.0)
205
203
  json (2.7.2)
206
204
  jwt (2.9.3)
data/README.md CHANGED
@@ -185,20 +185,6 @@ Usage: exe/failed-test-issues [options]
185
185
  -h, --help Show the usage
186
186
  ```
187
187
 
188
- ### `exe/existing-test-health-issue`
189
-
190
- ```shell
191
- Purpose: Checks whether tests coming from the rspec JSON report files has an existing test health issue opened.
192
- Usage: exe/existing-test-health-issue [options]
193
- -i, --input-files INPUT_FILES JSON rspec-retry report files
194
- -p, --project PROJECT Can be an integer or a group/project string
195
- -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
196
- --health-problem-type PROBLEM_TYPE
197
- Look for the given health problem type (failures, pass-after-retry, slow)
198
- -v, --version Show the version
199
- -h, --help Show the usage
200
- ```
201
-
202
188
  ### `exe/detect-infrastructure-failures`
203
189
 
204
190
  ```shell
data/exe/test-coverage CHANGED
@@ -3,21 +3,26 @@
3
3
 
4
4
  require "optparse"
5
5
  require "uri"
6
+ require "yaml"
6
7
 
7
8
  require_relative "../lib/gitlab_quality/test_tooling"
8
9
 
9
10
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owners'
10
11
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
11
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'
12
14
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data'
13
15
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
14
16
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
15
17
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
16
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'
17
20
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
21
+ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier'
22
+ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config'
18
23
 
19
24
  params = {}
20
- required_params = [:test_reports, :coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username]
25
+ required_params = [:test_reports, :coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username, :responsibility_patterns]
21
26
 
22
27
  options = OptionParser.new do |opts|
23
28
  opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
@@ -50,6 +55,14 @@ options = OptionParser.new do |opts|
50
55
  params[:clickhouse_username] = username
51
56
  end
52
57
 
58
+ opts.on('--clickhouse-shared-database DATABASE', 'ClickHouse shared database name (default: shared)') do |database|
59
+ params[:clickhouse_shared_database] = database
60
+ end
61
+
62
+ opts.on('--responsibility-patterns PATH', 'Path to YAML file with responsibility classification patterns') do |path|
63
+ params[:responsibility_patterns] = path
64
+ end
65
+
53
66
  opts.separator ""
54
67
  opts.separator "Environment variables:"
55
68
  opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)"
@@ -107,7 +120,9 @@ if params.any? && (required_params - params.keys).none?
107
120
 
108
121
  code_coverage_by_source_file = GitlabQuality::TestTooling::CodeCoverage::LcovFile.new(coverage_report).parsed_content
109
122
 
110
- source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
123
+ test_map_parser = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map)
124
+ source_file_to_tests = test_map_parser.source_to_tests
125
+ test_to_sources = test_map_parser.test_to_sources
111
126
 
112
127
  # Process test reports
113
128
  tests_to_categories = artifacts.test_reports.reduce({}) do |combined_hash, test_report_file|
@@ -121,12 +136,31 @@ if params.any? && (required_params - params.keys).none?
121
136
  source_file_classifier = GitlabQuality::TestTooling::CodeCoverage::SourceFileClassifier.new
122
137
  source_file_types = source_file_classifier.classify(code_coverage_by_source_file.keys)
123
138
 
139
+ # Load responsibility patterns from config file
140
+ begin
141
+ patterns_config = GitlabQuality::TestTooling::CodeCoverage::ResponsibilityPatternsConfig.new(
142
+ params[:responsibility_patterns]
143
+ )
144
+ rescue GitlabQuality::TestTooling::CodeCoverage::ResponsibilityPatternsConfig::ConfigError => e
145
+ puts "Error: #{e.message}"
146
+ exit 1
147
+ end
148
+
149
+ # Classify test files as responsible or dependent
150
+ responsibility_classifier = GitlabQuality::TestTooling::CodeCoverage::ResponsibilityClassifier.new(
151
+ test_to_sources,
152
+ responsible_patterns: patterns_config.responsible_patterns,
153
+ dependent_patterns: patterns_config.dependent_patterns
154
+ )
155
+ test_classifications = responsibility_classifier.classify_tests
156
+
124
157
  coverage_data = GitlabQuality::TestTooling::CodeCoverage::CoverageData.new(
125
158
  code_coverage_by_source_file,
126
159
  source_file_to_tests,
127
160
  tests_to_categories,
128
- category_owners.categories_to_teams,
129
- source_file_types
161
+ category_owners.feature_categories_to_teams,
162
+ source_file_types,
163
+ test_classifications
130
164
  )
131
165
 
132
166
  clickhouse_data = {
@@ -136,11 +170,15 @@ if params.any? && (required_params - params.keys).none?
136
170
  password: clickhouse_password
137
171
  }
138
172
 
139
- category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
140
- coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
173
+ shared_clickhouse_data = {
174
+ url: params[:clickhouse_url],
175
+ database: params[:clickhouse_shared_database] || 'shared',
176
+ username: params[:clickhouse_username],
177
+ password: clickhouse_password
178
+ }
141
179
 
142
- category_owners_table.create if ENV['CLICKHOUSE_CREATE_CATEGORY_OWNERS_TABLE'] == 'true'
143
- coverage_metrics_table.create if ENV['CLICKHOUSE_CREATE_COVERAGE_METRICS_TABLE'] == 'true'
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)
144
182
 
145
183
  if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
146
184
  category_owners_table.truncate
@@ -148,6 +186,11 @@ if params.any? && (required_params - params.keys).none?
148
186
  end
149
187
 
150
188
  coverage_metrics_table.push(coverage_data.as_db_table)
189
+
190
+ # Export test-to-file mappings
191
+ test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(test_to_sources)
192
+ test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
193
+ test_file_mappings_table.push(test_file_mapping_data.as_db_table)
151
194
  else
152
195
  puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
153
196
  puts options
@@ -0,0 +1,162 @@
1
+ # Code Coverage Module
2
+
3
+ Exports test coverage data to ClickHouse, enriched with:
4
+
5
+ - **Feature category ownership** - group, stage, and section for each covered file
6
+ - **Responsibility classification** - whether coverage comes from unit or integration tests
7
+ - **Test-to-file mappings** - which source files each test covers
8
+
9
+ ## How It Works
10
+
11
+ ### Feature Category Attribution
12
+
13
+ Coverage data is enriched with feature category ownership by joining three data sources:
14
+
15
+ 1. **Coverage Report** (LCOV) - which source files have coverage and their percentages
16
+ 2. **Test Map** - which test files cover each source file
17
+ 3. **Test Reports** (JSON) - which feature category each test file belongs to
18
+
19
+ ```mermaid
20
+ flowchart LR
21
+ subgraph Inputs
22
+ A["<b>Coverage Report</b><br/>user.rb: 85%"]
23
+ B["<b>Test Map</b><br/>user.rb → user_spec.rb"]
24
+ C["<b>Test Reports</b><br/>user_spec.rb → user_profile"]
25
+ end
26
+
27
+ subgraph Output
28
+ E["<b>ClickHouse Record</b><br/>file: user.rb<br/>feature_category: user_profile<br/>coverage: 85%"]
29
+ end
30
+
31
+ A --> D((Join))
32
+ B --> D
33
+ C --> D
34
+ D --> E
35
+ ```
36
+
37
+ This enables **multi-category attribution**: if a source file is covered by tests from
38
+ multiple feature categories, it creates a separate record for each category in ClickHouse.
39
+
40
+ ### Why All Three Inputs Are Required
41
+
42
+ | Input | Provides | Without it |
43
+ |-------|----------|------------|
44
+ | Coverage Report | Line/branch coverage percentages | No coverage metrics |
45
+ | Test Map | Source file → test file relationships | No feature category attribution (all records have `category=NULL`) |
46
+ | Test Reports | Test file → feature category metadata | No feature category attribution (all records have `category=NULL`) |
47
+
48
+ ## Responsibility Classification
49
+
50
+ Tests are classified as either **responsible** or **dependent**:
51
+
52
+ - **Responsible**: Unit tests that directly test a component in isolation
53
+ - **Dependent**: Integration/E2E tests that exercise a component through other layers
54
+
55
+ This classification is tracked per (source_file, feature_category) combination using two boolean columns:
56
+
57
+ | is_responsible | is_dependent | Meaning |
58
+ |----------------|--------------|---------|
59
+ | `true` | `true` | Source file has both unit AND integration test coverage from this feature category |
60
+ | `true` | `false` | Source file has only unit test coverage from this feature category |
61
+ | `false` | `true` | Source file has only integration test coverage from this feature category |
62
+ | `nil` | `nil` | No test mapping exists for this source file |
63
+
64
+ ### Configuration
65
+
66
+ This gem is designed to be reusable across different projects. Classification patterns
67
+ are project-specific and must be provided via a YAML config file, since different
68
+ codebases have different test directory structures. The config file defines regex
69
+ patterns for matching test file paths:
70
+
71
+ > **Note:** The table above describes the *semantic meaning* of the flags. The patterns
72
+ > you configure determine *which tests* produce those flags for your project.
73
+
74
+ ```yaml
75
+ # responsibility_patterns.yml
76
+ responsible:
77
+ - "^spec/(models|controllers|services)/" # Backend unit tests
78
+ - "^spec/frontend/" # Frontend unit tests
79
+ - "_test\\.go$" # Go unit tests
80
+
81
+ dependent:
82
+ - "^spec/(requests|features|integration)/" # Backend integration tests
83
+ - "^spec/frontend_integration/" # Frontend integration tests
84
+ - "^qa/" # E2E tests
85
+ - "_integration_test\\.go$" # Go integration tests
86
+ ```
87
+
88
+ **Pattern matching rules:**
89
+ 1. Dependent patterns are checked first (higher priority)
90
+ 2. If no pattern matches, the test defaults to "dependent"
91
+ 3. Patterns are Ruby regexes (escape special characters like `.` with `\\`)
92
+
93
+ **Why dependent has priority:** We use a conservative approach. `is_responsible: true`
94
+ makes a stronger claim ("this file has unit test coverage") than `is_dependent: true`.
95
+ If a test matches both patterns or no patterns, defaulting to "dependent" avoids
96
+ incorrectly inflating unit test coverage metrics. It's safer to under-claim than over-claim.
97
+
98
+ ### Example: GitLab Configuration
99
+
100
+ ```yaml
101
+ # .gitlab/coverage/responsibility_patterns.yml
102
+ responsible:
103
+ # Backend unit test directories
104
+ - "^spec/(models|controllers|services|workers|helpers|mailers|policies|presenters|uploaders|validators|lib|graphql|serializers|components)/"
105
+ - "^ee/spec/(models|controllers|services|workers|helpers|mailers|policies|presenters|uploaders|validators|lib|graphql|serializers|components)/"
106
+ # Frontend unit tests
107
+ - "^spec/frontend/"
108
+ - "^ee/spec/frontend/"
109
+ # Go unit tests
110
+ - "_test\\.go$"
111
+
112
+ dependent:
113
+ # Backend integration tests
114
+ - "^spec/(requests|features|system|integration)/"
115
+ - "^ee/spec/(requests|features|system|integration)/"
116
+ # Frontend integration tests
117
+ - "^spec/frontend_integration/"
118
+ - "^ee/spec/frontend_integration/"
119
+ # E2E tests
120
+ - "^qa/"
121
+ # Go integration tests
122
+ - "_integration_test\\.go$"
123
+ ```
124
+
125
+ ### Example: Standard Rails Project
126
+
127
+ ```yaml
128
+ # config/responsibility_patterns.yml
129
+ responsible:
130
+ - "^test/(models|controllers|services|helpers|mailers)/"
131
+ - "^test/unit/"
132
+
133
+ dependent:
134
+ - "^test/(integration|system)/"
135
+ - "^spec/features/"
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
+
147
+ ## CLI
148
+
149
+ Example usage:
150
+
151
+ ```bash
152
+ test-coverage \
153
+ --test-reports 'rspec/*.json' \
154
+ --coverage-report 'coverage/lcov.info' \
155
+ --test-map 'mapping.json' \
156
+ --responsibility-patterns 'config/responsibility_patterns.yml' \
157
+ --clickhouse-url 'https://clickhouse.example.com' \
158
+ --clickhouse-database 'coverage' \
159
+ --clickhouse-username 'user'
160
+ ```
161
+
162
+ See `exe/test-coverage --help` for full usage.
@@ -11,7 +11,8 @@ module GitlabQuality
11
11
  class Artifacts
12
12
  # Loads coverage artifacts from the filesystem
13
13
  #
14
- # @param test_reports [String] Glob pattern for test JSON report files (RSpec or Jest) (e.g., "reports/**/*.json")
14
+ # @param test_reports [String] Glob pattern(s) for test JSON report files (RSpec or Jest).
15
+ # Supports comma-separated patterns (e.g., "jest/**/*.json,rspec/**/*.json")
15
16
  # @param coverage_report [String] Path to the LCOV coverage report file (e.g., "coverage/lcov/gitlab.lcov")
16
17
  # @param test_map [String] Path to the test map file, gzipped or plain JSON (e.g., "crystalball/packed-mapping.json.gz")
17
18
  def initialize(coverage_report:, test_map:, test_reports:)
@@ -54,7 +55,9 @@ module GitlabQuality
54
55
 
55
56
  def test_reports_paths
56
57
  @test_reports_paths ||= begin
57
- paths = Dir.glob(@test_reports_glob)
58
+ # Support comma-separated glob patterns (e.g., "jest/**/*.json,rspec/**/*.json")
59
+ patterns = @test_reports_glob.split(',').map(&:strip).reject(&:empty?)
60
+ paths = Dir.glob(patterns)
58
61
 
59
62
  raise "No test reports found matching pattern: #{@test_reports_glob}" if paths.empty?
60
63
 
@@ -15,17 +15,17 @@ module GitlabQuality
15
15
  BASE_DELAY = 1 # seconds
16
16
  MAX_RETRIES = 3
17
17
 
18
- # @return [Hash] Category ownership hierarchy, section -> stage -> group -> [categories]
18
+ # @return [Hash] Feature category ownership hierarchy, section -> stage -> group -> [feature_categories]
19
19
  # @example Return value
20
20
  # {
21
21
  # "team_planning" => { # section
22
22
  # "project_management" => { # stage
23
23
  # "plan" => [ # group
24
- # "dev", # category
25
- # "service_desk" # category
24
+ # "dev", # feature_category
25
+ # "service_desk" # feature_category
26
26
  # ],
27
27
  # "product_planning" => [ # group
28
- # "portfolio_management", # category
28
+ # "portfolio_management", # feature_category
29
29
  # ...
30
30
  # ]
31
31
  # }
@@ -35,7 +35,7 @@ module GitlabQuality
35
35
  attr_reader :hierarchy
36
36
 
37
37
  def initialize
38
- @categories_map = {}
38
+ @feature_categories_map = {}
39
39
  @hierarchy = {}
40
40
 
41
41
  yaml_file = fetch_yaml_file
@@ -43,12 +43,12 @@ module GitlabQuality
43
43
  populate_ownership_hierarchy(yaml_content)
44
44
  end
45
45
 
46
- # @return [Array<Hash>] Flattened category ownership
46
+ # @return [Array<Hash>] Flattened feature category ownership
47
47
  # @example Return value
48
48
  # [
49
- # { category: "team_planning", group: "project_management", stage: "plan", section: "dev" },
50
- # { category: "service_desk", group: "project_management", stage: "plan", section: "dev" },
51
- # { category: "portfolio_management", group: "product_planning", stage: "plan", section: "dev" }
49
+ # { feature_category: "team_planning", group: "project_management", stage: "plan", section: "dev" },
50
+ # { feature_category: "service_desk", group: "project_management", stage: "plan", section: "dev" },
51
+ # { feature_category: "portfolio_management", group: "product_planning", stage: "plan", section: "dev" }
52
52
  # ...
53
53
  # ]
54
54
  def as_db_table
@@ -58,10 +58,10 @@ module GitlabQuality
58
58
  stages.each do |stage, groups|
59
59
  next unless groups
60
60
 
61
- groups.each do |group, categories|
62
- Array(categories).each do |category|
61
+ groups.each do |group, feature_categories|
62
+ Array(feature_categories).each do |feature_category|
63
63
  flattened_hierarchy << {
64
- category: category,
64
+ feature_category: feature_category,
65
65
  group: group,
66
66
  stage: stage,
67
67
  section: section
@@ -72,7 +72,7 @@ module GitlabQuality
72
72
  end
73
73
  end
74
74
 
75
- # @return [Hash] Mapping of categories to teams (i.e., groups, stages, sections)
75
+ # @return [Hash] Mapping of feature categories to teams (i.e., groups, stages, sections)
76
76
  # @example Return value
77
77
  # {
78
78
  # "team_planning" => { group: "project_management", stage: "plan", section: "dev" },
@@ -80,9 +80,9 @@ module GitlabQuality
80
80
  # "portfolio_management" => { group: "product_planning", stage: "plan", section: "dev" },
81
81
  # ...
82
82
  # }
83
- def categories_to_teams
84
- populate_categories_map(hierarchy)
85
- @categories_map
83
+ def feature_categories_to_teams
84
+ populate_feature_categories_map(hierarchy)
85
+ @feature_categories_map
86
86
  end
87
87
 
88
88
  private
@@ -126,25 +126,25 @@ module GitlabQuality
126
126
  @hierarchy[section][stage][group] = categories || []
127
127
  end
128
128
 
129
- def populate_categories_map(data, current_section = nil, current_stage = nil, current_group = nil)
129
+ def populate_feature_categories_map(data, current_section = nil, current_stage = nil, current_group = nil)
130
130
  case data
131
131
  when Hash # Sections / Stages / Groups
132
132
  data.each do |key, value|
133
133
  if current_section.nil? # Sections
134
- populate_categories_map(value, key, nil, nil)
134
+ populate_feature_categories_map(value, key, nil, nil)
135
135
  elsif current_stage.nil? # Stages
136
- populate_categories_map(value, current_section, key, nil)
136
+ populate_feature_categories_map(value, current_section, key, nil)
137
137
  elsif current_group.nil? # Groups
138
- populate_categories_map(value, current_section, current_stage, key)
139
- else # Categories
140
- populate_categories_map(value, current_section, current_stage, current_group)
138
+ populate_feature_categories_map(value, current_section, current_stage, key)
139
+ else # Feature categories
140
+ populate_feature_categories_map(value, current_section, current_stage, current_group)
141
141
  end
142
142
  end
143
- when Array # Categories array
144
- data.each do |category|
145
- next unless category.is_a?(String)
143
+ when Array # Feature categories array
144
+ data.each do |feature_category|
145
+ next unless feature_category.is_a?(String)
146
146
 
147
- @categories_map[category] = {
147
+ @feature_categories_map[feature_category] = {
148
148
  section: current_section,
149
149
  stage: current_stage,
150
150
  group: current_group
@@ -11,28 +11,7 @@ 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
14
+ KNOWN_UNOWNED = %w[shared not_owned tooling].freeze
36
15
 
37
16
  def truncate
38
17
  logger.debug("#{LOG_PREFIX} Truncating table #{full_table_name} ...")
@@ -42,14 +21,21 @@ module GitlabQuality
42
21
  logger.info("#{LOG_PREFIX} Successfully truncated table #{full_table_name}")
43
22
  end
44
23
 
45
- # Owners of particular category as group, stage and section
24
+ # Owners of particular feature category as group, stage and section
46
25
  #
47
- # @param category_name [String]
26
+ # @param feature_category_name [String] the feature_category name
48
27
  # @return [Hash]
49
- def owners(category_name)
50
- records.fetch(category_name)
28
+ def owners(feature_category_name)
29
+ if KNOWN_UNOWNED.include?(feature_category_name)
30
+ logger.info(
31
+ "#{LOG_PREFIX} #{feature_category_name} is a known feature category without owner..."
32
+ )
33
+ return {}
34
+ end
35
+
36
+ records.fetch(feature_category_name)
51
37
  rescue KeyError
52
- raise(MissingMappingError, "Category '#{category_name}' not found in table '#{table_name}'")
38
+ raise(MissingMappingError, "Feature category '#{feature_category_name}' not found in table '#{table_name}'")
53
39
  end
54
40
 
55
41
  private
@@ -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,37 +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
- category Nullable(String),
27
- ci_project_id Nullable(UInt32),
28
- ci_project_path Nullable(String),
29
- ci_job_name Nullable(String),
30
- ci_job_id Nullable(UInt64),
31
- ci_pipeline_id Nullable(UInt64),
32
- ci_merge_request_iid Nullable(UInt32),
33
- ci_branch Nullable(String),
34
- ci_target_branch Nullable(String)
35
- ) ENGINE = MergeTree()
36
- PARTITION BY toYYYYMM(timestamp)
37
- ORDER BY (ci_project_path, timestamp, file, ci_pipeline_id)
38
- SETTINGS index_granularity = 8192, allow_nullable_key = 1;
39
- SQL
40
-
41
- logger.info("#{LOG_PREFIX} Coverage metrics table created/verified successfully")
42
- end
43
-
44
12
  private
45
13
 
46
14
  # @return [Boolean] True if the record is valid, false otherwise
@@ -97,19 +65,13 @@ module GitlabQuality
97
65
  branch_coverage: record[:branch_coverage],
98
66
  function_coverage: record[:function_coverage],
99
67
  source_file_type: record[:source_file_type],
100
- category: record[:category],
68
+ is_responsible: record[:is_responsible],
69
+ is_dependent: record[:is_dependent],
70
+ category: record[:feature_category],
101
71
  **ci_metadata
102
72
  }
103
73
  end
104
74
 
105
- # @return [Time] Common timestamp for all coverage records
106
- def time
107
- @time ||= begin
108
- ci_created_at = ENV.fetch('CI_PIPELINE_CREATED_AT', nil)
109
- ci_created_at ? Time.strptime(ci_created_at, '%Y-%m-%dT%H:%M:%S%z') : Time.now.utc
110
- end
111
- end
112
-
113
75
  # @return [Hash] CI-related metadata
114
76
  def ci_metadata
115
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(