gitlab_quality-test_tooling 3.0.0 → 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: f037a73b3fd4a445324a37084915ea2d020475cca6f4e3b1d23045f290351082
4
- data.tar.gz: e32bc045856df09004e725b9b84317c74ffb66b59ed640b078e292f46a3ff565
3
+ metadata.gz: 32b52c2177aaf57f1ccf6a2bb922fd47e267b251ce74921c356f9631252051da
4
+ data.tar.gz: 8930c0fb427f3a946cd99902a230bcf90c48a051c9ffa1d19fb10a487107d231
5
5
  SHA512:
6
- metadata.gz: 400c1e51bf57a3f10cdb36c083444153326da8710edd828839881867aed4d9282da4b21fa16357f49bb498ac48199dda888e422e1ddf4f5453769cb5bdc92211
7
- data.tar.gz: cc071d0ca3c54a0d3408f6fc91165348c403ab5109c473d7bc518701a17f90d992f02139614dfb99fd53cac7e0ffef688241c7c232cf373344269ffe3e8b4436
6
+ metadata.gz: f265a7b7c74d4ef36e06c53a945b96ab066a4304acd0e6c439cf10afee6d0483de9253ca9290b279db7ec3c1d2323146da3aea8cf92aa57d4d1914c9c1479b1f
7
+ data.tar.gz: b983b542d66e9ac5b5775dd97b80ce6b464a618a5adf573f18613014dcfecc86555fc8b7d20e32b33b41b23ce9be255811cc276a50a1ee7945f0e4062deb15cf
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
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.3.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)
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,10 @@ options = OptionParser.new do |opts|
50
55
  params[:clickhouse_username] = username
51
56
  end
52
57
 
58
+ opts.on('--responsibility-patterns PATH', 'Path to YAML file with responsibility classification patterns') do |path|
59
+ params[:responsibility_patterns] = path
60
+ end
61
+
53
62
  opts.separator ""
54
63
  opts.separator "Environment variables:"
55
64
  opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)"
@@ -107,7 +116,9 @@ if params.any? && (required_params - params.keys).none?
107
116
 
108
117
  code_coverage_by_source_file = GitlabQuality::TestTooling::CodeCoverage::LcovFile.new(coverage_report).parsed_content
109
118
 
110
- source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
119
+ test_map_parser = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map)
120
+ source_file_to_tests = test_map_parser.source_to_tests
121
+ test_to_sources = test_map_parser.test_to_sources
111
122
 
112
123
  # Process test reports
113
124
  tests_to_categories = artifacts.test_reports.reduce({}) do |combined_hash, test_report_file|
@@ -121,12 +132,31 @@ if params.any? && (required_params - params.keys).none?
121
132
  source_file_classifier = GitlabQuality::TestTooling::CodeCoverage::SourceFileClassifier.new
122
133
  source_file_types = source_file_classifier.classify(code_coverage_by_source_file.keys)
123
134
 
135
+ # Load responsibility patterns from config file
136
+ begin
137
+ patterns_config = GitlabQuality::TestTooling::CodeCoverage::ResponsibilityPatternsConfig.new(
138
+ params[:responsibility_patterns]
139
+ )
140
+ rescue GitlabQuality::TestTooling::CodeCoverage::ResponsibilityPatternsConfig::ConfigError => e
141
+ puts "Error: #{e.message}"
142
+ exit 1
143
+ end
144
+
145
+ # Classify test files as responsible or dependent
146
+ responsibility_classifier = GitlabQuality::TestTooling::CodeCoverage::ResponsibilityClassifier.new(
147
+ test_to_sources,
148
+ responsible_patterns: patterns_config.responsible_patterns,
149
+ dependent_patterns: patterns_config.dependent_patterns
150
+ )
151
+ test_classifications = responsibility_classifier.classify_tests
152
+
124
153
  coverage_data = GitlabQuality::TestTooling::CodeCoverage::CoverageData.new(
125
154
  code_coverage_by_source_file,
126
155
  source_file_to_tests,
127
156
  tests_to_categories,
128
- category_owners.categories_to_teams,
129
- source_file_types
157
+ category_owners.feature_categories_to_teams,
158
+ source_file_types,
159
+ test_classifications
130
160
  )
131
161
 
132
162
  clickhouse_data = {
@@ -139,15 +169,17 @@ if params.any? && (required_params - params.keys).none?
139
169
  category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
140
170
  coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
141
171
 
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'
144
-
145
172
  if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
146
173
  category_owners_table.truncate
147
174
  category_owners_table.push(category_owners.as_db_table)
148
175
  end
149
176
 
150
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)
151
183
  else
152
184
  puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
153
185
  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,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
 
@@ -42,14 +19,14 @@ module GitlabQuality
42
19
  logger.info("#{LOG_PREFIX} Successfully truncated table #{full_table_name}")
43
20
  end
44
21
 
45
- # Owners of particular category as group, stage and section
22
+ # Owners of particular feature category as group, stage and section
46
23
  #
47
- # @param category_name [String]
24
+ # @param feature_category_name [String] the feature_category name
48
25
  # @return [Hash]
49
- def owners(category_name)
50
- records.fetch(category_name)
26
+ def owners(feature_category_name)
27
+ records.fetch(feature_category_name)
51
28
  rescue KeyError
52
- raise(MissingMappingError, "Category '#{category_name}' not found in table '#{table_name}'")
29
+ raise(MissingMappingError, "Feature category '#{feature_category_name}' not found in table '#{table_name}'")
53
30
  end
54
31
 
55
32
  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(
@@ -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
@@ -4,22 +4,31 @@ module GitlabQuality
4
4
  module TestTooling
5
5
  module CodeCoverage
6
6
  class CoverageData
7
+ RESPONSIBLE = 'responsible'
8
+ DEPENDENT = 'dependent'
9
+
7
10
  # @param [Hash<String, Hash>] code_coverage_by_source_file Source file
8
11
  # mapped to test coverage data
9
12
  # @param [Hash<String, Array<String>>] source_file_to_tests Source files
10
13
  # mapped to all test files testing them
11
- # @param [Hash<String, Array<String>>] tests_to_categories Test files
14
+ # @param [Hash<String, Array<String>>] tests_to_feature_categories Test files
12
15
  # mapped to all feature categories they belong to
13
- # @param [Hash<String, Hash>] categories_to_teams Mapping of categories
16
+ # @param [Hash<String, Hash>] feature_categories_to_teams Mapping of feature categories
14
17
  # to teams (i.e., groups, stages, sections)
15
18
  # @param [Hash<String, String>] source_file_types Mapping of source files
16
19
  # to their types (frontend, backend, etc.)
17
- def initialize(code_coverage_by_source_file, source_file_to_tests, tests_to_categories, categories_to_teams, source_file_types = {})
20
+ # @param [Hash<String, String>] test_classifications Mapping of test files
21
+ # to their responsibility classification (responsible or dependent)
22
+ def initialize(
23
+ code_coverage_by_source_file, source_file_to_tests, tests_to_feature_categories,
24
+ feature_categories_to_teams, source_file_types = {}, test_classifications = {}
25
+ )
18
26
  @code_coverage_by_source_file = code_coverage_by_source_file
19
27
  @source_file_to_tests = source_file_to_tests
20
- @tests_to_categories = tests_to_categories
21
- @categories_to_teams = categories_to_teams
28
+ @tests_to_feature_categories = tests_to_feature_categories
29
+ @feature_categories_to_teams = feature_categories_to_teams
22
30
  @source_file_types = source_file_types
31
+ @test_classifications = test_classifications
23
32
  end
24
33
 
25
34
  # @return [Array<Hash<Symbol, String>>] Mapping of column name to row
@@ -32,7 +41,9 @@ module GitlabQuality
32
41
  # branch_coverage: 95.0
33
42
  # function_coverage: 100.0
34
43
  # source_file_type: "backend"
35
- # category: "team_planning"
44
+ # is_responsible: true
45
+ # is_dependent: false
46
+ # feature_category: "team_planning"
36
47
  # group: "project_management"
37
48
  # stage: "plan"
38
49
  # section: "dev"
@@ -40,55 +51,81 @@ module GitlabQuality
40
51
  # ...
41
52
  # ]
42
53
  def as_db_table
43
- all_files.flat_map do |file|
44
- coverage_data = @code_coverage_by_source_file[file]
45
- line_coverage = coverage_data&.dig(:percentage)
46
- branch_coverage = coverage_data&.dig(:branch_percentage)
47
- function_coverage = coverage_data&.dig(:function_percentage)
48
-
49
- categories = categories_for(file)
50
- base_data = {
51
- file: file,
52
- line_coverage: line_coverage,
53
- branch_coverage: branch_coverage,
54
- function_coverage: function_coverage,
55
- source_file_type: @source_file_types[file] || 'other'
56
- }
57
-
58
- if categories.empty?
59
- base_data.merge(no_owner_info)
60
- else
61
- categories.map do |category|
62
- base_data.merge(owner_info(category))
63
- end
54
+ all_files.flat_map { |file| records_for_file(file) }
55
+ end
56
+
57
+ private
58
+
59
+ def records_for_file(file)
60
+ base_data = base_data_for(file)
61
+ feature_categories_with_flags = feature_categories_with_responsibility_flags_for(file)
62
+
63
+ if feature_categories_with_flags.empty?
64
+ base_data.merge(no_owner_info).merge(is_responsible: nil, is_dependent: nil)
65
+ else
66
+ feature_categories_with_flags.map do |feature_category, flags|
67
+ base_data.merge(owner_info(feature_category)).merge(
68
+ is_responsible: flags[:is_responsible],
69
+ is_dependent: flags[:is_dependent]
70
+ )
64
71
  end
65
72
  end
66
73
  end
67
74
 
68
- private
75
+ def base_data_for(file)
76
+ coverage_data = @code_coverage_by_source_file[file]
77
+
78
+ {
79
+ file: file,
80
+ line_coverage: coverage_data&.dig(:percentage),
81
+ branch_coverage: coverage_data&.dig(:branch_percentage),
82
+ function_coverage: coverage_data&.dig(:function_percentage),
83
+ source_file_type: @source_file_types[file] || 'other'
84
+ }
85
+ end
69
86
 
70
87
  def no_owner_info
71
88
  {
72
- category: nil,
89
+ feature_category: nil,
73
90
  group: nil,
74
91
  stage: nil,
75
92
  section: nil
76
93
  }
77
94
  end
78
95
 
79
- def owner_info(category)
80
- owner_info = @categories_to_teams[category]
96
+ def owner_info(feature_category)
97
+ owner_info = @feature_categories_to_teams[feature_category]
81
98
 
82
99
  {
83
- category: category,
100
+ feature_category: feature_category,
84
101
  group: owner_info&.dig(:group),
85
102
  stage: owner_info&.dig(:stage),
86
103
  section: owner_info&.dig(:section)
87
104
  }
88
105
  end
89
106
 
90
- def categories_for(file)
91
- @source_file_to_tests[file]&.flat_map { |test_file| @tests_to_categories[test_file] || [] }&.uniq || []
107
+ # Returns a hash of feature_category => { is_responsible: bool, is_dependent: bool }
108
+ # for a given source file. A feature category can have both flags true if it has
109
+ # both unit tests (responsible) and integration/E2E tests (dependent).
110
+ def feature_categories_with_responsibility_flags_for(file)
111
+ test_files = @source_file_to_tests[file] || []
112
+ return {} if test_files.empty?
113
+
114
+ test_files.each_with_object({}) do |test_file, feature_category_to_flags|
115
+ feature_categories = @tests_to_feature_categories[test_file] || []
116
+ classification = @test_classifications[test_file]
117
+
118
+ feature_categories.each do |feature_category|
119
+ feature_category_to_flags[feature_category] ||= { is_responsible: false, is_dependent: false }
120
+
121
+ case classification
122
+ when RESPONSIBLE
123
+ feature_category_to_flags[feature_category][:is_responsible] = true
124
+ when DEPENDENT
125
+ feature_category_to_flags[feature_category][:is_dependent] = true
126
+ end
127
+ end
128
+ end
92
129
  end
93
130
 
94
131
  def all_files
@@ -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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module CodeCoverage
6
+ class ResponsibilityClassifier
7
+ RESPONSIBLE = 'responsible'
8
+ DEPENDENT = 'dependent'
9
+
10
+ # @param test_to_sources [Hash<String, Array<String>>] Test files mapped to source files they cover
11
+ # @param responsible_patterns [Array<Regexp>] Patterns for unit tests
12
+ # @param dependent_patterns [Array<Regexp>] Patterns for integration/E2E tests
13
+ def initialize(test_to_sources, responsible_patterns:, dependent_patterns:)
14
+ @test_to_sources = test_to_sources
15
+ @responsible_patterns = responsible_patterns
16
+ @dependent_patterns = dependent_patterns
17
+ end
18
+
19
+ # Classifies each test file as responsible or dependent
20
+ # @return [Hash<String, String>] Test file path => classification
21
+ def classify_tests
22
+ @test_to_sources.keys.each_with_object({}) do |test_file, result|
23
+ result[test_file] = classify_test(test_file)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # Classifies a test file as responsible (unit) or dependent (integration/E2E).
30
+ #
31
+ # Dependent patterns are checked first because it's the safer default:
32
+ # - is_responsible: true claims "this file has unit test coverage"
33
+ # - is_dependent: true claims "this file has integration test coverage"
34
+ #
35
+ # If uncertain (overlapping patterns or no match), we default to dependent
36
+ # to avoid incorrectly inflating unit test coverage metrics.
37
+ def classify_test(test_file)
38
+ return DEPENDENT if @dependent_patterns.any? { |p| test_file.match?(p) }
39
+ return RESPONSIBLE if @responsible_patterns.any? { |p| test_file.match?(p) }
40
+
41
+ # Default to dependent for unknown test types
42
+ DEPENDENT
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module CodeCoverage
8
+ class ResponsibilityPatternsConfig
9
+ ConfigError = Class.new(StandardError)
10
+
11
+ attr_reader :responsible_patterns, :dependent_patterns
12
+
13
+ # @param file_path [String] Path to YAML config file
14
+ # @raise [ConfigError] if file cannot be loaded or parsed
15
+ def initialize(file_path)
16
+ @file_path = file_path
17
+ @config = load_config
18
+ @responsible_patterns = parse_patterns('responsible')
19
+ @dependent_patterns = parse_patterns('dependent')
20
+ end
21
+
22
+ private
23
+
24
+ def load_config
25
+ YAML.load_file(@file_path)
26
+ rescue Errno::ENOENT
27
+ raise ConfigError, "Config file not found: #{@file_path}"
28
+ rescue Psych::SyntaxError => e
29
+ raise ConfigError, "Invalid YAML syntax in #{@file_path}: #{e.message}"
30
+ end
31
+
32
+ def parse_patterns(key)
33
+ patterns = @config[key]
34
+
35
+ raise ConfigError, "Missing or invalid '#{key}' key in #{@file_path}. Expected an array of patterns." unless patterns.is_a?(Array)
36
+
37
+ patterns.map do |pattern|
38
+ Regexp.new(pattern)
39
+ rescue RegexpError => e
40
+ raise ConfigError, "Invalid regex pattern '#{pattern}' in #{@file_path}: #{e.message}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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
@@ -52,6 +52,7 @@ module GitlabQuality
52
52
  :skip_record_proc,
53
53
  :test_retried_proc,
54
54
  :custom_metrics_proc,
55
+ :spec_file_path_prefix,
55
56
  :logger
56
57
 
57
58
  # rubocop:disable Style/TrivialAccessors -- allows documenting that setting config enables the export as well as document input class type
@@ -108,6 +109,13 @@ module GitlabQuality
108
109
  @extra_rspec_metadata_keys ||= []
109
110
  end
110
111
 
112
+ # Extra path prefix for constructing full file path within mono-repository setups
113
+ #
114
+ # @return [String]
115
+ def spec_file_path_prefix
116
+ @spec_file_path_prefix ||= ""
117
+ end
118
+
111
119
  # A lambda that determines whether to skip recording a test result
112
120
  #
113
121
  # This is useful when you would want to skip initial failure when retrying specs is set up in a separate process
@@ -46,12 +46,15 @@ module GitlabQuality
46
46
  status: example.execution_result.status,
47
47
  run_time: (example.execution_result.run_time * 1000).round,
48
48
  location: example_location,
49
- exception_class: exception_class,
49
+ # TODO: remove exception_class once migration to exception_classes is fully complete on clickhouse side
50
+ exception_class: example.execution_result.exception&.class&.to_s,
51
+ exception_classes: exception_classes.map { |e| e.class.to_s }.uniq,
50
52
  failure_exception: failure_exception,
51
53
  quarantined: quarantined?,
52
54
  feature_category: example.metadata[:feature_category] || "",
53
55
  test_retried: config.test_retried_proc.call(example),
54
- run_type: run_type
56
+ run_type: run_type,
57
+ spec_file_path_prefix: config.spec_file_path_prefix
55
58
  }
56
59
  end
57
60
 
@@ -127,22 +130,25 @@ module GitlabQuality
127
130
  @file_path ||= example_location.gsub(/:\d+$/, "")
128
131
  end
129
132
 
130
- # Failure exception class
133
+ # Failure exception classes
131
134
  #
132
- # @return [String]
133
- def exception_class
134
- example.execution_result.exception&.class&.to_s
135
+ # @return [Array<Exception>]
136
+ def exception_classes
137
+ exception = example.execution_result.exception
138
+ return [] unless exception
139
+ return [exception] unless exception.respond_to?(:all_exceptions)
140
+
141
+ exception.all_exceptions.flatten
135
142
  end
136
143
 
137
144
  # Truncated exception stacktrace
138
145
  #
139
146
  # @return [String]
140
147
  def failure_exception
141
- example.execution_result.exception.then do |exception|
142
- next unless exception
148
+ exception = example.execution_result.exception
149
+ return unless exception
143
150
 
144
- exception.to_s.tr("\n", " ").slice(0, 1000)
145
- end
151
+ exception.to_s.tr("\n", " ").slice(0, 1000)
146
152
  end
147
153
 
148
154
  # Test run type | suite name
@@ -65,6 +65,7 @@ module GitlabQuality
65
65
  test_retried Bool,
66
66
  feature_category LowCardinality(String) DEFAULT 'unknown',
67
67
  run_type LowCardinality(String) DEFAULT 'unknown',
68
+ spec_file_path_prefix LowCardinality(String) DEFAULT '',
68
69
  ci_project_id UInt32,
69
70
  ci_job_name LowCardinality(String),
70
71
  ci_job_id UInt64,
@@ -75,6 +76,7 @@ module GitlabQuality
75
76
  ci_target_branch LowCardinality(String),
76
77
  ci_server_url LowCardinality(String) DEFAULT 'https://gitlab.com',
77
78
  exception_class String DEFAULT '',
79
+ exception_classes Array(String) DEFAULT [],
78
80
  failure_exception String DEFAULT ''
79
81
  )
80
82
  ENGINE = MergeTree()
@@ -8,6 +8,7 @@ module GitlabQuality
8
8
  "Net::ReadTimeout",
9
9
  "403 Forbidden - Your account has been blocked",
10
10
  "API failed (502) with `GitLab is not responding",
11
+ "Error Code: 502",
11
12
  "unexpected token at 'GitLab is not responding'",
12
13
  "GitLab: Internal API error (502).",
13
14
  "could not be found (502)",
@@ -127,14 +128,26 @@ module GitlabQuality
127
128
  end
128
129
 
129
130
  def full_stacktrace
131
+ page_error_failure = ""
132
+ first_non_ignored_failure = ""
133
+
130
134
  failures.each do |failure|
131
135
  message = failure['message'] || ""
132
136
  message_lines = failure['message_lines'] || []
133
137
 
134
138
  next if IGNORED_FAILURES.any? { |e| message.include?(e) }
135
139
 
136
- return message_lines.empty? ? message : message_lines.join("\n")
140
+ formatted_failure = message_lines.empty? ? message : message_lines.join("\n")
141
+
142
+ if message.include?("PageErrorChecker")
143
+ page_error_failure = formatted_failure
144
+ elsif first_non_ignored_failure.empty?
145
+ first_non_ignored_failure = formatted_failure
146
+ end
137
147
  end
148
+
149
+ # Return PageErrorChecker failure if found, otherwise first non-ignored failure
150
+ page_error_failure.empty? ? first_non_ignored_failure : page_error_failure
138
151
  end
139
152
 
140
153
  def calls_shared_examples?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.0.0"
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.0.0
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-11-17 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
@@ -227,9 +227,6 @@ dependencies:
227
227
  - - ">="
228
228
  - !ruby/object:Gem::Version
229
229
  version: '7.0'
230
- - - "<"
231
- - !ruby/object:Gem::Version
232
- version: '7.3'
233
230
  type: :runtime
234
231
  prerelease: false
235
232
  version_requirements: !ruby/object:Gem::Requirement
@@ -237,9 +234,6 @@ dependencies:
237
234
  - - ">="
238
235
  - !ruby/object:Gem::Version
239
236
  version: '7.0'
240
- - - "<"
241
- - !ruby/object:Gem::Version
242
- version: '7.3'
243
237
  - !ruby/object:Gem::Dependency
244
238
  name: amatch
245
239
  requirement: !ruby/object:Gem::Requirement
@@ -490,15 +484,20 @@ files:
490
484
  - lefthook.yml
491
485
  - lib/gitlab_quality/test_tooling.rb
492
486
  - lib/gitlab_quality/test_tooling/click_house/client.rb
487
+ - lib/gitlab_quality/test_tooling/code_coverage/README.md
493
488
  - lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb
494
489
  - lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb
495
490
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb
496
491
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb
497
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
498
494
  - lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb
499
495
  - lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
496
+ - lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb
497
+ - lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb
500
498
  - lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
501
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
502
501
  - lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
503
502
  - lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
504
503
  - lib/gitlab_quality/test_tooling/code_coverage/utils.rb