gitlab_quality-test_tooling 2.27.0 → 3.1.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 -2
- data/exe/test-coverage +15 -30
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +5 -31
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +44 -13
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +2 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +23 -5
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +93 -15
- data/lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb +94 -0
- 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 +16 -10
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +2 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +3 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be0541b0d2e39102c7665bb733cdd068467959cf727bee7cafc385d2117b2689
|
|
4
|
+
data.tar.gz: 0dcc0e738cc51add35954ad281e7c0a86aa8662956e6133644f72bafd28b03a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9f3927bc5646c115610015be3b780408518de92b8ebd741fc9270605fbf93e4a976b7eeb460a0d988b11cbadbfe154ccdb937da24f05637ae5574cb2cdd0f18
|
|
7
|
+
data.tar.gz: 9c641bc6e4f22c57fd16b5481a47d09ac45eac346b0b52da7f70ba3f4f4954d8e25e56fa54e9a8df5f939da95fb54c09d8dafc0d9f9e6e3733e7e1bc9e85da37
|
data/Gemfile.lock
CHANGED
data/exe/test-coverage
CHANGED
|
@@ -12,12 +12,12 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/c
|
|
|
12
12
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data'
|
|
13
13
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
14
14
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
15
|
-
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/rspec_report'
|
|
16
15
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
|
|
17
16
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
|
|
17
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
|
|
18
18
|
|
|
19
19
|
params = {}
|
|
20
|
-
required_params = [:coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username]
|
|
20
|
+
required_params = [:test_reports, :coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username]
|
|
21
21
|
|
|
22
22
|
options = OptionParser.new do |opts|
|
|
23
23
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
@@ -26,14 +26,10 @@ options = OptionParser.new do |opts|
|
|
|
26
26
|
opts.separator "Options:"
|
|
27
27
|
|
|
28
28
|
opts.on('--test-reports GLOB',
|
|
29
|
-
'Glob pattern for test JSON reports (RSpec or Jest) (e.g., "reports/**/*.json")
|
|
29
|
+
'Glob pattern for test JSON reports (RSpec or Jest) (e.g., "reports/**/*.json")') do |pattern|
|
|
30
30
|
params[:test_reports] = pattern
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
opts.on('--rspec-reports GLOB', '[DEPRECATED] Use --test-reports instead. Glob pattern for RSpec JSON reports. Ignored if --test-reports is also provided.') do |pattern|
|
|
34
|
-
params[:rspec_reports] = pattern
|
|
35
|
-
end
|
|
36
|
-
|
|
37
33
|
opts.on('--coverage-report PATH', 'Path to the LCOV coverage report (e.g., "coverage/lcov/gitlab.lcov")') do |path|
|
|
38
34
|
params[:coverage_report] = path
|
|
39
35
|
end
|
|
@@ -100,17 +96,10 @@ if params.any? && (required_params - params.keys).none?
|
|
|
100
96
|
exit 1
|
|
101
97
|
end
|
|
102
98
|
|
|
103
|
-
# Validate that at least one of test_reports or rspec_reports is provided
|
|
104
|
-
if params[:test_reports].nil? && params[:rspec_reports].nil?
|
|
105
|
-
puts "Error: At least one of --test-reports or --rspec-reports must be provided"
|
|
106
|
-
exit 1
|
|
107
|
-
end
|
|
108
|
-
|
|
109
99
|
artifacts = GitlabQuality::TestTooling::CodeCoverage::Artifacts.new(
|
|
110
100
|
coverage_report: params[:coverage_report],
|
|
111
101
|
test_map: params[:test_map],
|
|
112
|
-
test_reports: params[:test_reports]
|
|
113
|
-
rspec_reports: params[:rspec_reports]
|
|
102
|
+
test_reports: params[:test_reports]
|
|
114
103
|
)
|
|
115
104
|
|
|
116
105
|
coverage_report = artifacts.coverage_report
|
|
@@ -120,28 +109,24 @@ if params.any? && (required_params - params.keys).none?
|
|
|
120
109
|
|
|
121
110
|
source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
|
|
122
111
|
|
|
123
|
-
# Process test reports
|
|
124
|
-
tests_to_categories =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
|
|
129
|
-
end
|
|
130
|
-
else
|
|
131
|
-
# Fall back to legacy RspecReport for backward compatibility
|
|
132
|
-
artifacts.rspec_reports.reduce({}) do |combined_hash, rspec_report_file|
|
|
133
|
-
file_categories = GitlabQuality::TestTooling::CodeCoverage::RspecReport.new(rspec_report_file).tests_to_categories
|
|
134
|
-
combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
|
|
135
|
-
end
|
|
136
|
-
end
|
|
112
|
+
# Process test reports
|
|
113
|
+
tests_to_categories = artifacts.test_reports.reduce({}) do |combined_hash, test_report_file|
|
|
114
|
+
file_categories = GitlabQuality::TestTooling::CodeCoverage::TestReport.new(test_report_file).tests_to_categories
|
|
115
|
+
combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
|
|
116
|
+
end
|
|
137
117
|
|
|
138
118
|
category_owners = GitlabQuality::TestTooling::CodeCoverage::CategoryOwners.new
|
|
139
119
|
|
|
120
|
+
# Classify source files by type (frontend, backend, etc.)
|
|
121
|
+
source_file_classifier = GitlabQuality::TestTooling::CodeCoverage::SourceFileClassifier.new
|
|
122
|
+
source_file_types = source_file_classifier.classify(code_coverage_by_source_file.keys)
|
|
123
|
+
|
|
140
124
|
coverage_data = GitlabQuality::TestTooling::CodeCoverage::CoverageData.new(
|
|
141
125
|
code_coverage_by_source_file,
|
|
142
126
|
source_file_to_tests,
|
|
143
127
|
tests_to_categories,
|
|
144
|
-
category_owners.categories_to_teams
|
|
128
|
+
category_owners.categories_to_teams,
|
|
129
|
+
source_file_types
|
|
145
130
|
)
|
|
146
131
|
|
|
147
132
|
clickhouse_data = {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'stringio'
|
|
5
5
|
require 'zlib'
|
|
6
|
+
require 'active_support/core_ext/object/blank'
|
|
6
7
|
|
|
7
8
|
module GitlabQuality
|
|
8
9
|
module TestTooling
|
|
@@ -10,13 +11,13 @@ module GitlabQuality
|
|
|
10
11
|
class Artifacts
|
|
11
12
|
# Loads coverage artifacts from the filesystem
|
|
12
13
|
#
|
|
13
|
-
# @param test_reports [String
|
|
14
|
-
# @param rspec_reports [String, nil] [DEPRECATED] Use test_reports instead. Glob pattern for RSpec JSON report files
|
|
14
|
+
# @param test_reports [String] Glob pattern for test JSON report files (RSpec or Jest) (e.g., "reports/**/*.json")
|
|
15
15
|
# @param coverage_report [String] Path to the LCOV coverage report file (e.g., "coverage/lcov/gitlab.lcov")
|
|
16
16
|
# @param test_map [String] Path to the test map file, gzipped or plain JSON (e.g., "crystalball/packed-mapping.json.gz")
|
|
17
|
-
def initialize(coverage_report:, test_map:, test_reports:
|
|
17
|
+
def initialize(coverage_report:, test_map:, test_reports:)
|
|
18
|
+
raise ArgumentError, "test_reports cannot be blank" if test_reports.blank?
|
|
19
|
+
|
|
18
20
|
@test_reports_glob = test_reports
|
|
19
|
-
@rspec_reports_glob = rspec_reports
|
|
20
21
|
@coverage_report_path = coverage_report
|
|
21
22
|
@test_map_path = test_map
|
|
22
23
|
end
|
|
@@ -33,19 +34,6 @@ module GitlabQuality
|
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
# Loads and parses RSpec JSON report files
|
|
37
|
-
#
|
|
38
|
-
# @deprecated Use {#test_reports} instead
|
|
39
|
-
# @return [Array<Hash>] Array of parsed JSON RSpec reports
|
|
40
|
-
# @raise [RuntimeError] If no RSpec reports are found or if JSON parsing fails
|
|
41
|
-
def rspec_reports
|
|
42
|
-
@rspec_report_files ||= rspec_reports_paths.map do |report_path|
|
|
43
|
-
JSON.parse(File.read(report_path))
|
|
44
|
-
rescue JSON::ParserError => e
|
|
45
|
-
raise "Invalid JSON in RSpec report file #{report_path}: #{e.message}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
37
|
# Loads the LCOV coverage report file
|
|
50
38
|
#
|
|
51
39
|
# @return [String] Raw content of the LCOV coverage report
|
|
@@ -65,8 +53,6 @@ module GitlabQuality
|
|
|
65
53
|
private
|
|
66
54
|
|
|
67
55
|
def test_reports_paths
|
|
68
|
-
return [] if @test_reports_glob.nil? || @test_reports_glob.empty?
|
|
69
|
-
|
|
70
56
|
@test_reports_paths ||= begin
|
|
71
57
|
paths = Dir.glob(@test_reports_glob)
|
|
72
58
|
|
|
@@ -76,18 +62,6 @@ module GitlabQuality
|
|
|
76
62
|
end
|
|
77
63
|
end
|
|
78
64
|
|
|
79
|
-
def rspec_reports_paths
|
|
80
|
-
return [] if @rspec_reports_glob.nil? || @rspec_reports_glob.empty?
|
|
81
|
-
|
|
82
|
-
@rspec_reports_paths ||= begin
|
|
83
|
-
paths = Dir.glob(@rspec_reports_glob)
|
|
84
|
-
|
|
85
|
-
raise "No RSpec reports found matching pattern: #{@rspec_reports_glob}" if paths.empty?
|
|
86
|
-
|
|
87
|
-
paths
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
65
|
def read_coverage_reports
|
|
92
66
|
raise "Coverage report not found in: #{@coverage_report_path}" unless File.exist?(@coverage_report_path)
|
|
93
67
|
|
|
@@ -19,7 +19,10 @@ module GitlabQuality
|
|
|
19
19
|
CREATE TABLE IF NOT EXISTS #{table_name} (
|
|
20
20
|
timestamp DateTime64(6, 'UTC'),
|
|
21
21
|
file String,
|
|
22
|
-
|
|
22
|
+
line_coverage Float64,
|
|
23
|
+
branch_coverage Nullable(Float64),
|
|
24
|
+
function_coverage Nullable(Float64),
|
|
25
|
+
source_file_type String,
|
|
23
26
|
category Nullable(String),
|
|
24
27
|
ci_project_id Nullable(UInt32),
|
|
25
28
|
ci_project_path Nullable(String),
|
|
@@ -42,22 +45,47 @@ module GitlabQuality
|
|
|
42
45
|
|
|
43
46
|
# @return [Boolean] True if the record is valid, false otherwise
|
|
44
47
|
def valid_record?(record)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
valid_file?(record) &&
|
|
49
|
+
valid_line_coverage?(record) &&
|
|
50
|
+
valid_branch_coverage?(record) &&
|
|
51
|
+
valid_function_coverage?(record)
|
|
52
|
+
end
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
# @return [Boolean] True if the file field is present
|
|
55
|
+
def valid_file?(record)
|
|
56
|
+
return true unless record[:file].nil?
|
|
57
|
+
|
|
58
|
+
logger.warn("#{LOG_PREFIX} Skipping record with nil file: #{record}")
|
|
59
|
+
false
|
|
60
|
+
end
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
# @return [Boolean] True if line_coverage is present and not NaN
|
|
63
|
+
def valid_line_coverage?(record)
|
|
64
|
+
if record[:line_coverage].nil?
|
|
65
|
+
logger.warn("#{LOG_PREFIX} Skipping record with nil line_coverage: #{record}")
|
|
57
66
|
return false
|
|
58
67
|
end
|
|
59
68
|
|
|
60
|
-
true
|
|
69
|
+
return true unless record[:line_coverage].nan?
|
|
70
|
+
|
|
71
|
+
logger.warn("#{LOG_PREFIX} Skipping record with NaN line_coverage: #{record}")
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Boolean] True if branch_coverage is not NaN (or is nil)
|
|
76
|
+
def valid_branch_coverage?(record)
|
|
77
|
+
return true unless record[:branch_coverage]&.nan?
|
|
78
|
+
|
|
79
|
+
logger.warn("#{LOG_PREFIX} Skipping record with NaN branch_coverage: #{record}")
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean] True if function_coverage is not NaN (or is nil)
|
|
84
|
+
def valid_function_coverage?(record)
|
|
85
|
+
return true unless record[:function_coverage]&.nan?
|
|
86
|
+
|
|
87
|
+
logger.warn("#{LOG_PREFIX} Skipping record with NaN function_coverage: #{record}")
|
|
88
|
+
false
|
|
61
89
|
end
|
|
62
90
|
|
|
63
91
|
# @return [Hash] Transformed coverage data including timestamp and CI metadata
|
|
@@ -65,7 +93,10 @@ module GitlabQuality
|
|
|
65
93
|
{
|
|
66
94
|
timestamp: time,
|
|
67
95
|
file: record[:file],
|
|
68
|
-
|
|
96
|
+
line_coverage: record[:line_coverage],
|
|
97
|
+
branch_coverage: record[:branch_coverage],
|
|
98
|
+
function_coverage: record[:function_coverage],
|
|
99
|
+
source_file_type: record[:source_file_type],
|
|
69
100
|
category: record[:category],
|
|
70
101
|
**ci_metadata
|
|
71
102
|
}
|
|
@@ -23,6 +23,8 @@ module GitlabQuality
|
|
|
23
23
|
logger.debug("#{LOG_PREFIX} Starting data export to ClickHouse")
|
|
24
24
|
sanitized_data = sanitize(data)
|
|
25
25
|
|
|
26
|
+
return logger.warn("#{LOG_PREFIX} No valid data found after sanitization, skipping ClickHouse export!") if sanitized_data.empty?
|
|
27
|
+
|
|
26
28
|
client.insert_json_data(table_name, sanitized_data)
|
|
27
29
|
logger.info("#{LOG_PREFIX} Successfully pushed #{sanitized_data.size} records to #{full_table_name}!")
|
|
28
30
|
rescue StandardError => e
|
|
@@ -12,11 +12,14 @@ module GitlabQuality
|
|
|
12
12
|
# mapped to all feature categories they belong to
|
|
13
13
|
# @param [Hash<String, Hash>] categories_to_teams Mapping of categories
|
|
14
14
|
# to teams (i.e., groups, stages, sections)
|
|
15
|
-
|
|
15
|
+
# @param [Hash<String, String>] source_file_types Mapping of source files
|
|
16
|
+
# 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 = {})
|
|
16
18
|
@code_coverage_by_source_file = code_coverage_by_source_file
|
|
17
19
|
@source_file_to_tests = source_file_to_tests
|
|
18
20
|
@tests_to_categories = tests_to_categories
|
|
19
21
|
@categories_to_teams = categories_to_teams
|
|
22
|
+
@source_file_types = source_file_types
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
# @return [Array<Hash<Symbol, String>>] Mapping of column name to row
|
|
@@ -25,7 +28,10 @@ module GitlabQuality
|
|
|
25
28
|
# [
|
|
26
29
|
# {
|
|
27
30
|
# file: "app/channels/application_cable/channel.rb"
|
|
28
|
-
#
|
|
31
|
+
# line_coverage: 100.0
|
|
32
|
+
# branch_coverage: 95.0
|
|
33
|
+
# function_coverage: 100.0
|
|
34
|
+
# source_file_type: "backend"
|
|
29
35
|
# category: "team_planning"
|
|
30
36
|
# group: "project_management"
|
|
31
37
|
# stage: "plan"
|
|
@@ -35,13 +41,25 @@ module GitlabQuality
|
|
|
35
41
|
# ]
|
|
36
42
|
def as_db_table
|
|
37
43
|
all_files.flat_map do |file|
|
|
38
|
-
|
|
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
|
+
|
|
39
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
|
+
|
|
40
58
|
if categories.empty?
|
|
41
|
-
|
|
59
|
+
base_data.merge(no_owner_info)
|
|
42
60
|
else
|
|
43
61
|
categories.map do |category|
|
|
44
|
-
|
|
62
|
+
base_data.merge(owner_info(category))
|
|
45
63
|
end
|
|
46
64
|
end
|
|
47
65
|
end
|
|
@@ -17,7 +17,13 @@ module GitlabQuality
|
|
|
17
17
|
# branch_coverage: {},
|
|
18
18
|
# total_lines: 2,
|
|
19
19
|
# covered_lines: 1,
|
|
20
|
-
# percentage: 50.0
|
|
20
|
+
# percentage: 50.0,
|
|
21
|
+
# total_branches: 4,
|
|
22
|
+
# covered_branches: 3,
|
|
23
|
+
# branch_percentage: 75.0,
|
|
24
|
+
# total_functions: 2,
|
|
25
|
+
# covered_functions: 1,
|
|
26
|
+
# function_percentage: 50.0
|
|
21
27
|
# },
|
|
22
28
|
# ...
|
|
23
29
|
# }
|
|
@@ -27,18 +33,7 @@ module GitlabQuality
|
|
|
27
33
|
@parsed_content = {}
|
|
28
34
|
@current_file = nil
|
|
29
35
|
|
|
30
|
-
@lcov_file_content.each_line
|
|
31
|
-
case line
|
|
32
|
-
when /^SF:(.+)$/
|
|
33
|
-
register_source_file(::Regexp.last_match(1))
|
|
34
|
-
when /^DA:(\d+),(\d+)$/
|
|
35
|
-
register_line_data(::Regexp.last_match(1), ::Regexp.last_match(2))
|
|
36
|
-
when /^BRDA:(\d+),(\d+),(\d+),(-|\d+)$/
|
|
37
|
-
register_branch_data(::Regexp.last_match(1), ::Regexp.last_match(4))
|
|
38
|
-
when /^end_of_record$/
|
|
39
|
-
@current_file = nil
|
|
40
|
-
end
|
|
41
|
-
end
|
|
36
|
+
@lcov_file_content.each_line { |line| parse_line(line) }
|
|
42
37
|
|
|
43
38
|
include_coverage
|
|
44
39
|
@parsed_content
|
|
@@ -46,10 +41,32 @@ module GitlabQuality
|
|
|
46
41
|
|
|
47
42
|
private
|
|
48
43
|
|
|
44
|
+
def parse_line(line)
|
|
45
|
+
case line
|
|
46
|
+
when /^SF:(.+)$/
|
|
47
|
+
register_source_file(::Regexp.last_match(1))
|
|
48
|
+
when /^DA:(\d+),(\d+)$/
|
|
49
|
+
register_line_data(::Regexp.last_match(1), ::Regexp.last_match(2))
|
|
50
|
+
when /^BRDA:(\d+),(\d+),(\d+),(-|\d+)$/
|
|
51
|
+
register_branch_data(::Regexp.last_match(1), ::Regexp.last_match(4))
|
|
52
|
+
when /^FNF:(\d+)$/
|
|
53
|
+
register_functions_found(::Regexp.last_match(1))
|
|
54
|
+
when /^FNH:(\d+)$/
|
|
55
|
+
register_functions_hit(::Regexp.last_match(1))
|
|
56
|
+
when /^BRF:(\d+)$/
|
|
57
|
+
register_branches_found(::Regexp.last_match(1))
|
|
58
|
+
when /^BRH:(\d+)$/
|
|
59
|
+
register_branches_hit(::Regexp.last_match(1))
|
|
60
|
+
when /^end_of_record$/
|
|
61
|
+
@current_file = nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
49
65
|
def include_coverage
|
|
50
66
|
@parsed_content.each_key do |file|
|
|
51
|
-
# TODO: Support branch coverage too
|
|
52
67
|
@parsed_content[file].merge!(line_coverage_for(file))
|
|
68
|
+
@parsed_content[file].merge!(branch_coverage_for(file))
|
|
69
|
+
@parsed_content[file].merge!(function_coverage_for(file))
|
|
53
70
|
end
|
|
54
71
|
end
|
|
55
72
|
|
|
@@ -69,7 +86,12 @@ module GitlabQuality
|
|
|
69
86
|
|
|
70
87
|
def register_source_file(filename)
|
|
71
88
|
@current_file = filename.gsub(%r{^\./}, '')
|
|
72
|
-
@parsed_content[@current_file] = {
|
|
89
|
+
@parsed_content[@current_file] = {
|
|
90
|
+
line_coverage: {},
|
|
91
|
+
branch_coverage: {},
|
|
92
|
+
branches: { found: 0, hit: 0 },
|
|
93
|
+
functions: { found: 0, hit: 0 }
|
|
94
|
+
}
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def register_line_data(line_no, count)
|
|
@@ -85,6 +107,62 @@ module GitlabQuality
|
|
|
85
107
|
@parsed_content[@current_file][:branch_coverage][line_no.to_i] ||= []
|
|
86
108
|
@parsed_content[@current_file][:branch_coverage][line_no.to_i] << taken_count
|
|
87
109
|
end
|
|
110
|
+
|
|
111
|
+
def register_functions_found(count)
|
|
112
|
+
return unless @current_file
|
|
113
|
+
|
|
114
|
+
@parsed_content[@current_file][:functions][:found] = count.to_i
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def register_functions_hit(count)
|
|
118
|
+
return unless @current_file
|
|
119
|
+
|
|
120
|
+
@parsed_content[@current_file][:functions][:hit] = count.to_i
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def register_branches_found(count)
|
|
124
|
+
return unless @current_file
|
|
125
|
+
|
|
126
|
+
@parsed_content[@current_file][:branches][:found] = count.to_i
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def register_branches_hit(count)
|
|
130
|
+
return unless @current_file
|
|
131
|
+
|
|
132
|
+
@parsed_content[@current_file][:branches][:hit] = count.to_i
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def branch_coverage_for(file)
|
|
136
|
+
data = @parsed_content[file]
|
|
137
|
+
return {} unless data && data[:branches]
|
|
138
|
+
|
|
139
|
+
total = data[:branches][:found]
|
|
140
|
+
covered = data[:branches][:hit]
|
|
141
|
+
|
|
142
|
+
return {} if total.nil? || total.zero?
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
total_branches: total,
|
|
146
|
+
covered_branches: covered,
|
|
147
|
+
branch_percentage: (covered.to_f / total * 100).round(2)
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def function_coverage_for(file)
|
|
152
|
+
data = @parsed_content[file]
|
|
153
|
+
return {} unless data && data[:functions]
|
|
154
|
+
|
|
155
|
+
total = data[:functions][:found]
|
|
156
|
+
covered = data[:functions][:hit]
|
|
157
|
+
|
|
158
|
+
return {} if total.nil? || total.zero?
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
total_functions: total,
|
|
162
|
+
covered_functions: covered,
|
|
163
|
+
function_percentage: (covered.to_f / total * 100).round(2)
|
|
164
|
+
}
|
|
165
|
+
end
|
|
88
166
|
end
|
|
89
167
|
end
|
|
90
168
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module CodeCoverage
|
|
6
|
+
class SourceFileClassifier
|
|
7
|
+
PATTERNS = {
|
|
8
|
+
'frontend' => [
|
|
9
|
+
%r{^app/assets/javascripts/.*\.(js|jsx|ts|tsx|vue)$},
|
|
10
|
+
%r{^app/assets/stylesheets/.*\.(css|scss)$},
|
|
11
|
+
%r{^ee/app/assets/javascripts/.*\.(js|jsx|ts|tsx|vue)$},
|
|
12
|
+
%r{^ee/app/assets/stylesheets/.*\.(css|scss)$},
|
|
13
|
+
%r{^jh/app/assets/javascripts/.*\.(js|jsx|ts|tsx|vue)$},
|
|
14
|
+
%r{^spec/frontend/},
|
|
15
|
+
%r{^ee/spec/frontend/},
|
|
16
|
+
%r{^spec/frontend_integration/},
|
|
17
|
+
%r{^app/assets/javascripts/.*\.graphql$},
|
|
18
|
+
/\.stories\.js$/
|
|
19
|
+
],
|
|
20
|
+
'backend' => [
|
|
21
|
+
%r{^app/(models|controllers|services|workers|helpers|mailers|policies|presenters|uploaders|validators|enums|events|experiments|facades|channels)/.*\.rb$},
|
|
22
|
+
%r{^app/serializers/.*\.rb$},
|
|
23
|
+
%r{^app/graphql/.*\.rb$},
|
|
24
|
+
%r{^app/components/.*\.rb$},
|
|
25
|
+
%r{^app/views/.*\.(haml|erb)$},
|
|
26
|
+
%r{^lib/.*\.rb$},
|
|
27
|
+
%r{^lib/api/.*\.rb$},
|
|
28
|
+
%r{^ee/app/.*\.rb$},
|
|
29
|
+
%r{^ee/lib/.*\.rb$},
|
|
30
|
+
%r{^jh/app/.*\.rb$},
|
|
31
|
+
%r{^jh/lib/.*\.rb$},
|
|
32
|
+
%r{^spec/.*_spec\.rb$},
|
|
33
|
+
%r{^ee/spec/.*_spec\.rb$},
|
|
34
|
+
%r{^lib/tasks/.*\.rake$}
|
|
35
|
+
],
|
|
36
|
+
'database' => [
|
|
37
|
+
%r{^db/migrate/.*\.rb$},
|
|
38
|
+
%r{^db/post_migrate/.*\.rb$},
|
|
39
|
+
%r{^ee/db/geo/migrate/.*\.rb$},
|
|
40
|
+
%r{^db/structure\.sql$},
|
|
41
|
+
%r{^db/seeds\.rb$},
|
|
42
|
+
%r{^db/fixtures/}
|
|
43
|
+
],
|
|
44
|
+
'infrastructure' => [
|
|
45
|
+
/^\.gitlab-ci\.yml$/,
|
|
46
|
+
%r{^\.gitlab/ci/.*\.(yml|yaml)$},
|
|
47
|
+
/Dockerfile/,
|
|
48
|
+
/\.dockerfile$/,
|
|
49
|
+
%r{^scripts/pipeline/}
|
|
50
|
+
],
|
|
51
|
+
'qa' => [
|
|
52
|
+
%r{^qa/.*\.rb$}
|
|
53
|
+
],
|
|
54
|
+
'workhorse' => [
|
|
55
|
+
%r{^workhorse/.*\.go$}
|
|
56
|
+
],
|
|
57
|
+
'tooling' => [
|
|
58
|
+
%r{^tooling/.*\.(rb|js)$},
|
|
59
|
+
%r{^rubocop/.*\.rb$},
|
|
60
|
+
%r{^danger/.*\.rb$},
|
|
61
|
+
/^\.rubocop\.yml$/
|
|
62
|
+
],
|
|
63
|
+
'configuration' => [
|
|
64
|
+
%r{^config/.*\.(yml|yaml|rb)$}
|
|
65
|
+
]
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
# Classifies a collection of file paths into their respective types
|
|
69
|
+
#
|
|
70
|
+
# @param file_paths [Array<String>] List of file paths to classify
|
|
71
|
+
# @return [Hash<String, String>] Hash mapping file path to file type
|
|
72
|
+
def classify(file_paths)
|
|
73
|
+
file_paths.each_with_object({}) do |file_path, result|
|
|
74
|
+
result[file_path] = determine_type(file_path)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Determines the type of a single file based on pattern matching
|
|
81
|
+
#
|
|
82
|
+
# @param file_path [String] The file path to classify
|
|
83
|
+
# @return [String] The file type category
|
|
84
|
+
def determine_type(file_path)
|
|
85
|
+
PATTERNS.each do |type, patterns|
|
|
86
|
+
return type if patterns.any? { |pattern| file_path.match?(pattern) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
'other'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
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
|
-
|
|
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
|
|
133
|
+
# Failure exception classes
|
|
131
134
|
#
|
|
132
|
-
# @return [
|
|
133
|
-
def
|
|
134
|
-
example.execution_result.exception
|
|
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
|
|
142
|
-
|
|
148
|
+
exception = example.execution_result.exception
|
|
149
|
+
return unless exception
|
|
143
150
|
|
|
144
|
-
|
|
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()
|
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:
|
|
4
|
+
version: 3.1.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
|
+
date: 2025-12-03 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
|
|
@@ -498,6 +492,7 @@ files:
|
|
|
498
492
|
- lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb
|
|
499
493
|
- lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
|
|
500
494
|
- lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
|
|
495
|
+
- lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb
|
|
501
496
|
- lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
|
|
502
497
|
- lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
|
|
503
498
|
- lib/gitlab_quality/test_tooling/code_coverage/utils.rb
|