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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +30 -28
  5. data/README.md +1 -1
  6. data/exe/epic-readiness-notification +58 -0
  7. data/exe/post-to-slack +4 -0
  8. data/exe/relate-failure-issue +9 -0
  9. data/exe/test-coverage +155 -0
  10. data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
  11. data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +92 -0
  12. data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +158 -0
  13. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +80 -0
  14. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +140 -0
  15. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +75 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +100 -0
  17. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +169 -0
  18. data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
  19. data/lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb +94 -0
  20. data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
  21. data/lib/gitlab_quality/test_tooling/code_coverage/test_report.rb +43 -0
  22. data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
  23. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  24. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  25. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  26. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  27. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
  28. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  29. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
  30. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
  31. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
  32. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
  33. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
  34. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
  35. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  36. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  37. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  38. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  39. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  40. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  41. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  42. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
  43. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  44. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  45. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  46. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
  47. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  48. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  49. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
  50. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
  51. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
  52. data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
  53. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
  54. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  55. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  56. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  57. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  58. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
  59. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
  60. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
  61. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +126 -80
  62. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +96 -0
  63. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
  64. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  65. data/lib/gitlab_quality/test_tooling.rb +3 -0
  66. metadata +84 -55
  67. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  68. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  69. 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| (issue.labels & [OPERATIONAL_READINESS_CHECKLIST_LABEL]).any? }
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