gitlab_quality-test_tooling 2.16.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/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/Gemfile.lock +30 -28
- data/README.md +1 -1
- data/exe/epic-readiness-notification +58 -0
- data/exe/post-to-slack +4 -0
- data/exe/relate-failure-issue +9 -0
- data/exe/test-coverage +155 -0
- data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +92 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +158 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +80 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +140 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +75 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +100 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +169 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb +94 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_report.rb +43 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
- data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
- data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
- data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
- data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
- data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
- data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
- data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +126 -80
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +96 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +3 -0
- metadata +84 -55
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/influxdb_tools.rb +0 -33
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module CodeCoverage
|
|
6
|
+
class LcovFile
|
|
7
|
+
# @param [String] lcov_file_content The content of the lcov file
|
|
8
|
+
def initialize(lcov_file_content)
|
|
9
|
+
@lcov_file_content = lcov_file_content
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Hash<String, Hash>] The parsed content of the lcov file
|
|
13
|
+
# @example Return value
|
|
14
|
+
# {
|
|
15
|
+
# "path/to/file1.rb" => {
|
|
16
|
+
# line_coverage: { 1 => 1, 2 => 0 },
|
|
17
|
+
# branch_coverage: {},
|
|
18
|
+
# total_lines: 2,
|
|
19
|
+
# covered_lines: 1,
|
|
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
|
|
27
|
+
# },
|
|
28
|
+
# ...
|
|
29
|
+
# }
|
|
30
|
+
def parsed_content
|
|
31
|
+
return @parsed_content if @parsed_content
|
|
32
|
+
|
|
33
|
+
@parsed_content = {}
|
|
34
|
+
@current_file = nil
|
|
35
|
+
|
|
36
|
+
@lcov_file_content.each_line { |line| parse_line(line) }
|
|
37
|
+
|
|
38
|
+
include_coverage
|
|
39
|
+
@parsed_content
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
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
|
+
|
|
65
|
+
def include_coverage
|
|
66
|
+
@parsed_content.each_key do |file|
|
|
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))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def line_coverage_for(file)
|
|
74
|
+
data = @parsed_content[file]
|
|
75
|
+
return unless data
|
|
76
|
+
|
|
77
|
+
total_lines = data[:line_coverage].size
|
|
78
|
+
covered_lines = data[:line_coverage].values.count(&:positive?)
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
total_lines: total_lines,
|
|
82
|
+
covered_lines: covered_lines,
|
|
83
|
+
percentage: total_lines.zero? ? 0.0 : (covered_lines.to_f / total_lines * 100).round(2)
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def register_source_file(filename)
|
|
88
|
+
@current_file = filename.gsub(%r{^\./}, '')
|
|
89
|
+
@parsed_content[@current_file] = {
|
|
90
|
+
line_coverage: {},
|
|
91
|
+
branch_coverage: {},
|
|
92
|
+
branches: { found: 0, hit: 0 },
|
|
93
|
+
functions: { found: 0, hit: 0 }
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def register_line_data(line_no, count)
|
|
98
|
+
return unless @current_file
|
|
99
|
+
|
|
100
|
+
@parsed_content[@current_file][:line_coverage][line_no.to_i] = count.to_i
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def register_branch_data(line_no, taken)
|
|
104
|
+
return unless @current_file
|
|
105
|
+
|
|
106
|
+
taken_count = taken == '-' ? 0 : taken.to_i
|
|
107
|
+
@parsed_content[@current_file][:branch_coverage][line_no.to_i] ||= []
|
|
108
|
+
@parsed_content[@current_file][:branch_coverage][line_no.to_i] << taken_count
|
|
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
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module CodeCoverage
|
|
8
|
+
class RspecReport
|
|
9
|
+
# @param [Hash<String, Object>] rspec_report The content of an RSpec
|
|
10
|
+
# report
|
|
11
|
+
def initialize(rspec_report)
|
|
12
|
+
@rspec_report = rspec_report
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash<String, String>>] Content of the "examples"
|
|
16
|
+
# section of the RSpec report
|
|
17
|
+
def examples
|
|
18
|
+
@examples ||= @rspec_report['examples']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Hash<String, Array<String>>] Test files mapped to all feature
|
|
22
|
+
# categories they belong to
|
|
23
|
+
# @example Return value
|
|
24
|
+
# {
|
|
25
|
+
# "spec/path/to/file_spec.rb" => [
|
|
26
|
+
# "feature_category1", "feature_category2"
|
|
27
|
+
# ],
|
|
28
|
+
# ...
|
|
29
|
+
# }
|
|
30
|
+
def tests_to_categories
|
|
31
|
+
@tests_to_categories ||= examples.to_a.filter_map do |example|
|
|
32
|
+
next unless example.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
file_path = example['file_path']
|
|
35
|
+
next unless file_path.is_a?(String)
|
|
36
|
+
|
|
37
|
+
[file_path.gsub('./', ''), Array(example['feature_category']).compact]
|
|
38
|
+
end.to_h
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
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
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module CodeCoverage
|
|
8
|
+
class TestMap
|
|
9
|
+
SEPARATOR = '/'
|
|
10
|
+
MARKER = 1
|
|
11
|
+
|
|
12
|
+
# @param [Hash] compact_map A nested hash structure where keys are
|
|
13
|
+
# source files and values are tree structures with test file paths
|
|
14
|
+
# @example Example of compact_map for a file tested by 3 spec files
|
|
15
|
+
# {
|
|
16
|
+
# "app/models/user.rb" => {
|
|
17
|
+
# "spec" => {
|
|
18
|
+
# "models" => {
|
|
19
|
+
# "user_spec.rb" => 1 # MARKER (1) indicates a leaf node
|
|
20
|
+
# }
|
|
21
|
+
# },
|
|
22
|
+
# "ee" => {
|
|
23
|
+
# "spec" => {
|
|
24
|
+
# "lib" => {
|
|
25
|
+
# "ee" => {
|
|
26
|
+
# "gitlab" => {
|
|
27
|
+
# "background_migration" => {
|
|
28
|
+
# "delete_invalid_epic_issues_spec.rb"=>1,
|
|
29
|
+
# "backfill_security_policies_spec.rb"=>1
|
|
30
|
+
# }
|
|
31
|
+
# }
|
|
32
|
+
# }
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
# }
|
|
36
|
+
# }
|
|
37
|
+
# }
|
|
38
|
+
def initialize(compact_map)
|
|
39
|
+
@compact_map = compact_map
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Hash<String, Array<String>>] Source files mapped to all test
|
|
43
|
+
# files testing them
|
|
44
|
+
# @example Return value
|
|
45
|
+
# {
|
|
46
|
+
# "path/to/file1.rb" => [
|
|
47
|
+
# "spec/path/to/file1_spec.rb",
|
|
48
|
+
# "spec/path/to/another/file1_spec.rb"
|
|
49
|
+
# ],
|
|
50
|
+
# ...
|
|
51
|
+
# }
|
|
52
|
+
def source_to_tests
|
|
53
|
+
@source_to_tests ||= @compact_map.transform_values { |tree| traverse(tree).to_a.uniq }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Hash<String, Array<String>>] Test files mapped to all source
|
|
57
|
+
# files tested by them
|
|
58
|
+
# @example Return value
|
|
59
|
+
# {
|
|
60
|
+
# "spec/path/to/file1_spec.rb" => [
|
|
61
|
+
# "path/to/file1.rb",
|
|
62
|
+
# "path/to/file2.rb"
|
|
63
|
+
# ],
|
|
64
|
+
# ...
|
|
65
|
+
# }
|
|
66
|
+
def test_to_sources
|
|
67
|
+
@test_to_sources ||= begin
|
|
68
|
+
test_to_sources = Hash.new { |hash, key| hash[key] = [] }
|
|
69
|
+
|
|
70
|
+
@compact_map.each do |source_file, tree|
|
|
71
|
+
traverse(tree).to_a.each do |test_file|
|
|
72
|
+
test_to_sources[test_file] << source_file
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
test_to_sources
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def traverse(tree, segments = [], &block)
|
|
83
|
+
return to_enum(__method__, tree, segments) unless block
|
|
84
|
+
return yield segments.join(SEPARATOR) if tree == MARKER && !segments.empty?
|
|
85
|
+
|
|
86
|
+
tree.each do |key, value|
|
|
87
|
+
traverse(value, segments + [key], &block)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module CodeCoverage
|
|
8
|
+
class TestReport
|
|
9
|
+
# @param [Hash<String, Object>] test_report The content of a test
|
|
10
|
+
# report (RSpec or Jest)
|
|
11
|
+
def initialize(test_report)
|
|
12
|
+
@test_report = test_report
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash<String, String>>] Content of the "examples"
|
|
16
|
+
# section of the test report
|
|
17
|
+
def examples
|
|
18
|
+
@examples ||= @test_report['examples']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Hash<String, Array<String>>] Test files mapped to all feature
|
|
22
|
+
# categories they belong to
|
|
23
|
+
# @example Return value
|
|
24
|
+
# {
|
|
25
|
+
# "spec/path/to/file_spec.rb" => [
|
|
26
|
+
# "feature_category1", "feature_category2"
|
|
27
|
+
# ],
|
|
28
|
+
# ...
|
|
29
|
+
# }
|
|
30
|
+
def tests_to_categories
|
|
31
|
+
@tests_to_categories ||= examples.to_a.filter_map do |example|
|
|
32
|
+
next unless example.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
file_path = example['file_path']
|
|
35
|
+
next unless file_path.is_a?(String)
|
|
36
|
+
|
|
37
|
+
[file_path.gsub('./', ''), Array(example['feature_category']).compact]
|
|
38
|
+
end.to_h
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module GitlabQuality
|
|
7
|
+
module TestTooling
|
|
8
|
+
module CodeCoverage
|
|
9
|
+
module Utils
|
|
10
|
+
def exponential_delay_with_jitter(attempt)
|
|
11
|
+
exponential_delay = (2**(attempt - 1))
|
|
12
|
+
jitter = rand # 0-1 seconds
|
|
13
|
+
exponential_delay + jitter
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -21,7 +21,7 @@ module GitlabQuality
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def has_operational_readiness_issue_linked?(linked_issue_iids, issue_client)
|
|
24
|
-
linked_issues(linked_issue_iids, issue_client).any? { |issue|
|
|
24
|
+
linked_issues(linked_issue_iids, issue_client).any? { |issue| issue.labels.intersect?([OPERATIONAL_READINESS_CHECKLIST_LABEL]) }
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def linked_issues(linked_issue_iids, issue_client)
|
|
@@ -118,6 +118,17 @@ module GitlabQuality
|
|
|
118
118
|
labels.first.id
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
def ids_for_group_labels(labels, group_labels_client)
|
|
122
|
+
labels.filter_map { |label| get_id_for_group_label(label, group_labels_client) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def get_id_for_group_label(label_name, group_labels_client)
|
|
126
|
+
labels = group_labels_client.group_labels(options: { search: label_name })
|
|
127
|
+
return nil if labels.empty?
|
|
128
|
+
|
|
129
|
+
labels.first.id
|
|
130
|
+
end
|
|
131
|
+
|
|
121
132
|
def extract_id_from_gid(gid)
|
|
122
133
|
gid.to_s.split('/').last.to_i
|
|
123
134
|
end
|