gitlab_quality-test_tooling 2.27.0 → 3.0.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 +1 -1
- 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_result/base_test_result.rb +1 -14
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f037a73b3fd4a445324a37084915ea2d020475cca6f4e3b1d23045f290351082
|
|
4
|
+
data.tar.gz: e32bc045856df09004e725b9b84317c74ffb66b59ed640b078e292f46a3ff565
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 400c1e51bf57a3f10cdb36c083444153326da8710edd828839881867aed4d9282da4b21fa16357f49bb498ac48199dda888e422e1ddf4f5453769cb5bdc92211
|
|
7
|
+
data.tar.gz: cc071d0ca3c54a0d3408f6fc91165348c403ab5109c473d7bc518701a17f90d992f02139614dfb99fd53cac7e0ffef688241c7c232cf373344269ffe3e8b4436
|
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
|
|
@@ -8,7 +8,6 @@ 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",
|
|
12
11
|
"unexpected token at 'GitLab is not responding'",
|
|
13
12
|
"GitLab: Internal API error (502).",
|
|
14
13
|
"could not be found (502)",
|
|
@@ -128,26 +127,14 @@ module GitlabQuality
|
|
|
128
127
|
end
|
|
129
128
|
|
|
130
129
|
def full_stacktrace
|
|
131
|
-
page_error_failure = ""
|
|
132
|
-
first_non_ignored_failure = ""
|
|
133
|
-
|
|
134
130
|
failures.each do |failure|
|
|
135
131
|
message = failure['message'] || ""
|
|
136
132
|
message_lines = failure['message_lines'] || []
|
|
137
133
|
|
|
138
134
|
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
|
139
135
|
|
|
140
|
-
|
|
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
|
|
136
|
+
return message_lines.empty? ? message : message_lines.join("\n")
|
|
147
137
|
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
|
|
151
138
|
end
|
|
152
139
|
|
|
153
140
|
def calls_shared_examples?
|
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.0.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-
|
|
11
|
+
date: 2025-11-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -498,6 +498,7 @@ files:
|
|
|
498
498
|
- lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb
|
|
499
499
|
- lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
|
|
500
500
|
- lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
|
|
501
|
+
- lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb
|
|
501
502
|
- lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
|
|
502
503
|
- lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
|
|
503
504
|
- lib/gitlab_quality/test_tooling/code_coverage/utils.rb
|