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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -4
- data/README.md +0 -14
- data/exe/test-coverage +51 -8
- data/lib/gitlab_quality/test_tooling/code_coverage/README.md +162 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +5 -2
- data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +26 -26
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +13 -27
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +3 -41
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +17 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +48 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +71 -34
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +11 -1
- data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb +47 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb +46 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +33 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +2 -4
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +4 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +4 -4
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +8 -0
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +36 -10
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +2 -0
- data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_formatter.rb +38 -0
- data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_helper.rb +76 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +15 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +9 -28
- data/exe/existing-test-health-issue +0 -59
- data/exe/generate-test-session +0 -70
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +0 -288
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0afeda440d33c3cb9e1e81adf43ce462d12b604cffdf96b04dbe83a9a4d1f853
|
|
4
|
+
data.tar.gz: 1a638b0a0b409ed28cb94da821a59f73967b6ae55cad7e87552a096b9c5c525e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
5
|
-
activesupport (>= 7.0
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
143
|
-
coverage_metrics_table
|
|
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)
|
|
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
|
-
|
|
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]
|
|
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", #
|
|
25
|
-
# "service_desk" #
|
|
24
|
+
# "dev", # feature_category
|
|
25
|
+
# "service_desk" # feature_category
|
|
26
26
|
# ],
|
|
27
27
|
# "product_planning" => [ # group
|
|
28
|
-
# "portfolio_management", #
|
|
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
|
-
@
|
|
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
|
-
# {
|
|
50
|
-
# {
|
|
51
|
-
# {
|
|
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,
|
|
62
|
-
Array(
|
|
61
|
+
groups.each do |group, feature_categories|
|
|
62
|
+
Array(feature_categories).each do |feature_category|
|
|
63
63
|
flattened_hierarchy << {
|
|
64
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
@
|
|
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
|
|
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
|
-
|
|
134
|
+
populate_feature_categories_map(value, key, nil, nil)
|
|
135
135
|
elsif current_stage.nil? # Stages
|
|
136
|
-
|
|
136
|
+
populate_feature_categories_map(value, current_section, key, nil)
|
|
137
137
|
elsif current_group.nil? # Groups
|
|
138
|
-
|
|
139
|
-
else #
|
|
140
|
-
|
|
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 #
|
|
144
|
-
data.each do |
|
|
145
|
-
next unless
|
|
143
|
+
when Array # Feature categories array
|
|
144
|
+
data.each do |feature_category|
|
|
145
|
+
next unless feature_category.is_a?(String)
|
|
146
146
|
|
|
147
|
-
@
|
|
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
|
-
|
|
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
|
|
26
|
+
# @param feature_category_name [String] the feature_category name
|
|
48
27
|
# @return [Hash]
|
|
49
|
-
def owners(
|
|
50
|
-
|
|
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, "
|
|
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
|
-
|
|
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(
|