gitlab_quality-test_tooling 2.16.0 → 2.25.1

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 (67) 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 +113 -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 +77 -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 +62 -0
  14. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +109 -0
  15. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +73 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +82 -0
  17. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +91 -0
  18. data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
  19. data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -0
  20. data/lib/gitlab_quality/test_tooling/code_coverage/utils.rb +18 -0
  21. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +1 -1
  22. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +11 -0
  23. data/lib/gitlab_quality/test_tooling/feature_readiness/epic_readiness_notifier.rb +308 -0
  24. data/lib/gitlab_quality/test_tooling/gcs_tools.rb +49 -0
  25. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +2 -9
  26. data/lib/gitlab_quality/test_tooling/gitlab_client/group_labels_client.rb +34 -0
  27. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +1 -1
  28. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +2 -2
  29. data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +1 -1
  30. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +1 -1
  31. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +2 -2
  32. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +2 -2
  33. data/lib/gitlab_quality/test_tooling/report/group_issues/error_message_normalizer.rb +49 -0
  34. data/lib/gitlab_quality/test_tooling/report/group_issues/error_pattern_matcher.rb +36 -0
  35. data/lib/gitlab_quality/test_tooling/report/group_issues/failure_processor.rb +73 -0
  36. data/lib/gitlab_quality/test_tooling/report/group_issues/group_results_in_issues.rb +48 -0
  37. data/lib/gitlab_quality/test_tooling/report/group_issues/incident_checker.rb +61 -0
  38. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_base.rb +48 -0
  39. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_creator.rb +44 -0
  40. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_finder.rb +81 -0
  41. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_formatter.rb +83 -0
  42. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_manager.rb +33 -0
  43. data/lib/gitlab_quality/test_tooling/report/group_issues/issue_updater.rb +87 -0
  44. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +6 -3
  45. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  46. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +2 -6
  47. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +176 -5
  48. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
  49. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +2 -1
  50. data/lib/gitlab_quality/test_tooling/runtime/env.rb +9 -4
  51. data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +103 -3
  52. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +1 -1
  53. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +1 -1
  54. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +1 -1
  55. data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +1 -1
  56. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +39 -11
  57. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +115 -15
  58. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/formatter.rb +61 -36
  59. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +125 -80
  60. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +95 -0
  61. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +6 -2
  62. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  63. data/lib/gitlab_quality/test_tooling.rb +3 -0
  64. metadata +82 -55
  65. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/log_test_metrics.rb +0 -117
  66. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +0 -49
  67. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/influxdb_tools.rb +0 -33
@@ -3,6 +3,10 @@
3
3
  require 'nokogiri'
4
4
  require 'rubygems/text'
5
5
 
6
+ require_relative 'group_issues/error_pattern_matcher'
7
+ require_relative 'group_issues/error_message_normalizer'
8
+ require_relative 'group_issues/group_results_in_issues'
9
+
6
10
  module GitlabQuality
7
11
  module TestTooling
8
12
  module Report
@@ -12,6 +16,7 @@ module GitlabQuality
12
16
  # - Takes a project where failure issues should be created
13
17
  # - Find issue by title (with test description or test file), then further filter by stack trace, then pick the better-matching one
14
18
  # - Add the failed job to the issue description, and update labels
19
+ # - Can group similar failures together when group_similar option is enabled
15
20
  class RelateFailureIssue < ReportAsIssue
16
21
  include TestTooling::Concerns::FindSetDri
17
22
  include Concerns::GroupAndCategoryLabels
@@ -23,7 +28,7 @@ module GitlabQuality
23
28
  FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
24
29
  ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*\n###/m
25
30
 
26
- NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2 automation:bot-authored type::maintenance]).freeze
31
+ NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2 automation:bot-authored type::maintenance suppress-contributor-links]).freeze
27
32
  SCREENSHOT_IGNORED_ERRORS = ['500 Internal Server Error', 'fabricate_via_api!', 'Error Code 500'].freeze
28
33
  FAILURE_ISSUE_GUIDE_URL = "https://handbook.gitlab.com/handbook/engineering/testing/guide-to-e2e-test-failure-issues/"
29
34
  FAILURE_ISSUE_HANDBOOK_GUIDE = "**:rotating_light: [End-to-End Test Failure Issue Debugging Guide](#{FAILURE_ISSUE_GUIDE_URL}) :rotating_light:**\n".freeze
@@ -31,13 +36,15 @@ module GitlabQuality
31
36
  # there before being released to the public repository
32
37
  DIFF_PROJECT_MAPPINGS = {
33
38
  'gitlab-org/quality/e2e-test-issues' => 'gitlab-org/security/gitlab',
39
+ 'gitlab-org/quality/test-failure-issues' => 'gitlab-org/security/gitlab',
34
40
  'gitlab-org/gitlab' => 'gitlab-org/security/gitlab',
35
41
  'gitlab-org/customers-gitlab-com' => 'gitlab-org/customers-gitlab-com'
36
42
  }.freeze
37
43
 
38
44
  # Don't use the E2E test issues project for commit parent
39
45
  COMMIT_PROJECT_MAPPINGS = {
40
- 'gitlab-org/quality/e2e-test-issues' => 'gitlab-org/gitlab'
46
+ 'gitlab-org/quality/e2e-test-issues' => 'gitlab-org/gitlab',
47
+ 'gitlab-org/quality/test-failure-issues' => 'gitlab-org/gitlab'
41
48
  }.freeze
42
49
 
43
50
  # The project contains record of the deployments we use to determine the commit diff
@@ -63,6 +70,8 @@ module GitlabQuality
63
70
  base_issue_labels: nil,
64
71
  exclude_labels_for_search: nil,
65
72
  metrics_files: [],
73
+ group_similar: false,
74
+ environment_issues_output_file: nil,
66
75
  **kwargs)
67
76
  super
68
77
  @max_diff_ratio = max_diff_ratio.to_f
@@ -72,21 +81,116 @@ module GitlabQuality
72
81
  @issue_type = 'issue'
73
82
  @commented_issue_list = Set.new
74
83
  @metrics_files = Array(metrics_files)
84
+ @group_similar = group_similar
85
+ @environment_issues_output_file = environment_issues_output_file
75
86
  end
76
87
 
77
88
  private
78
89
 
79
- attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search, :metrics_files, :ops_gitlab_client
90
+ attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search, :metrics_files, :ops_gitlab_client, :group_similar,
91
+ :environment_issues_output_file
80
92
 
81
93
  def run!
82
94
  puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
83
95
 
96
+ run_with_grouping! if group_similar
97
+
98
+ return if similar_issues_grouped?
99
+
84
100
  TestResults::Builder.new(token: token, project: project, file_glob: files).test_results_per_file do |test_results|
85
101
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
86
102
  process_test_results(test_results)
87
103
  end
88
104
  end
89
105
 
106
+ def similar_issues_grouped?
107
+ grouper.summary[:grouped_issues].positive?
108
+ end
109
+
110
+ def grouper
111
+ @grouper ||= GitlabQuality::TestTooling::Report::GroupIssues::GroupResultsInIssues.new(
112
+ gitlab: gitlab,
113
+ config: grouper_config
114
+ )
115
+ end
116
+
117
+ def run_with_grouping!
118
+ Runtime::Logger.info "=> Grouping similar failures where possible"
119
+
120
+ all_test_results = collect_all_test_results
121
+ return if all_test_results.empty?
122
+
123
+ Runtime::Logger.info "=> Processing #{all_test_results.count} failures with GroupResultsInIssues"
124
+
125
+ failure_data = convert_test_results_to_failure_data(all_test_results)
126
+
127
+ grouper.process_failures(failure_data)
128
+ grouper.process_issues
129
+
130
+ export_environment_issues_for_slack(environment_issues_output_file) if environment_issues_output_file
131
+ end
132
+
133
+ def collect_all_test_results
134
+ all_test_results = []
135
+
136
+ TestResults::Builder.new(token: token, project: project, file_glob: files).test_results_per_file do |test_results|
137
+ Runtime::Logger.info "=> Collecting #{test_results.count} tests from #{test_results.path}"
138
+ all_test_results.concat(test_results.select(&:failures?))
139
+ end
140
+
141
+ all_test_results
142
+ end
143
+
144
+ def convert_test_results_to_failure_data(test_results)
145
+ test_results.map do |test|
146
+ {
147
+ description: test.name,
148
+ full_description: test.name,
149
+ file_path: test.relative_file,
150
+ line_number: extract_line_number(test),
151
+ exception: extract_exception_data(test),
152
+ ci_job_url: test.ci_job_url,
153
+ testcase: extract_test_id_or_name(test),
154
+ product_group: extract_product_group(test),
155
+ level: extract_level(test)
156
+ }
157
+ end
158
+ end
159
+
160
+ def extract_line_number(test)
161
+ test.respond_to?(:line_number) ? test.line_number : nil
162
+ end
163
+
164
+ def extract_exception_data(test)
165
+ {
166
+ 'message' => test.failures.first&.dig('message') || test.full_stacktrace || 'Unknown error'
167
+ }
168
+ end
169
+
170
+ def extract_product_group(test)
171
+ test.respond_to?(:product_group) ? test.product_group : nil
172
+ end
173
+
174
+ def extract_level(test)
175
+ test.respond_to?(:level) ? test.level : nil
176
+ end
177
+
178
+ def grouper_config
179
+ {
180
+ thresholds: {
181
+ min_failures_to_group: 2
182
+ }
183
+ }
184
+ end
185
+
186
+ def extract_test_id_or_name(test)
187
+ return test.example_id if test.respond_to?(:example_id)
188
+ return test.id if test.respond_to?(:id)
189
+ return test.name if test.respond_to?(:name)
190
+
191
+ "#{test.relative_file}:#{test.respond_to?(:line_number) ? test.line_number : 'unknown'}"
192
+ end
193
+
90
194
  def new_issue_labels(test)
91
195
  up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS + group_and_category_labels_for_test(test))
92
196
  end
@@ -469,10 +573,40 @@ module GitlabQuality
469
573
  end
470
574
 
471
575
  def new_issue_assignee_id(test)
472
- return unless test.product_group?
576
+ assignee_id = try_feature_category_assignment(test)
577
+ return assignee_id if assignee_id
578
+
579
+ try_product_group_assignment(test)
580
+ end
581
+
582
+ def try_feature_category_assignment(test)
583
+ unless test.respond_to?(:feature_category) && test.feature_category?
584
+ Runtime::Logger.info("No feature_category found for DRI assignment")
585
+ return
586
+ end
587
+
588
+ labels_inference = GitlabQuality::TestTooling::LabelsInference.new
589
+ product_group = labels_inference.product_group_from_feature_category(test.feature_category)
590
+
591
+ unless product_group
592
+ Runtime::Logger.warn("Could not map feature_category '#{test.feature_category}' to product_group")
593
+ return
594
+ end
595
+
596
+ dri = test_dri(product_group, test.stage, test.section)
597
+ Runtime::Logger.info("Assigning #{dri} as DRI for the issue (via feature_category).")
598
+
599
+ gitlab.find_user_id(username: dri)
600
+ end
601
+
602
+ def try_product_group_assignment(test)
603
+ unless test.respond_to?(:product_group) && test.product_group?
604
+ Runtime::Logger.info("No product_group found for DRI assignment")
605
+ return
606
+ end
473
607
 
474
608
  dri = test_dri(test.product_group, test.stage, test.section)
475
- puts " => Assigning #{dri} as DRI for the issue."
609
+ Runtime::Logger.info("Assigning #{dri} as DRI for the issue (via product_group).")
476
610
 
477
611
  gitlab.find_user_id(username: dri)
478
612
  end
@@ -561,6 +695,43 @@ module GitlabQuality
561
695
 
562
696
  "|| [Screenshot](#{ci_job_url}/artifacts/file/#{screenshot_path})"
563
697
  end
698
+
699
+ def export_environment_issues_for_slack(output_file)
700
+ return unless similar_issues_grouped?
701
+
702
+ File.write(output_file, build_environment_issues_data.to_json)
703
+ rescue StandardError => e
704
+ puts "Warning: Failed to export environment issues for Slack: #{e.message}"
705
+ end
706
+
707
+ def build_environment_issues_data
708
+ {
709
+ grouped_failures: format_grouped_failures,
710
+ summary: grouper.summary
711
+ }
712
+ end
713
+
714
+ def format_grouped_failures
715
+ grouper.grouped_failures.map do |_fingerprint, grouped_failure|
716
+ {
717
+ fingerprint: grouped_failure[:fingerprint],
718
+ pattern_name: grouped_failure[:pattern_name],
719
+ normalized_message: grouped_failure[:normalized_message],
720
+ failure_count: grouped_failure[:failures].size,
721
+ failures: format_individual_failures(grouped_failure[:failures])
722
+ }
723
+ end
724
+ end
725
+
726
+ def format_individual_failures(failures)
727
+ failures.map do |failure|
728
+ {
729
+ description: failure[:description],
730
+ file_path: failure[:file_path],
731
+ ci_job_url: failure[:ci_job_url]
732
+ }
733
+ end
734
+ end
564
735
  end
565
736
  end
566
737
  end
@@ -104,7 +104,6 @@ module GitlabQuality
104
104
  description: new_issue_description(test),
105
105
  labels: new_issue_labels(test).to_a,
106
106
  issue_type: issue_type,
107
- assignee_id: new_issue_assignee_id(test),
108
107
  due_date: new_issue_due_date(test),
109
108
  confidential: confidential
110
109
  }.compact
@@ -11,7 +11,8 @@ module GitlabQuality
11
11
  # - Add test metadata, duration to the issue with group and category labels
12
12
  class SlowTestIssue < HealthProblemReporter
13
13
  IDENTITY_LABELS = ['test', 'rspec:slow test', 'test-health:slow', 'rspec profiling', 'automation:bot-authored'].freeze
14
- NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
14
+ NEW_ISSUE_LABELS = Set.new(
15
+ ['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3', 'suppress-contributor-links', *IDENTITY_LABELS]).freeze
15
16
  REPORT_SECTION_HEADER = '### Slowness reports'
16
17
  REPORTS_DOCUMENTATION = <<~DOC
17
18
  Slow tests were detected, please see the [test speed best practices guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed)
@@ -15,16 +15,17 @@ module GitlabQuality
15
15
  'CI_JOB_ID' => :ci_job_id,
16
16
  'CI_JOB_NAME' => :ci_job_name,
17
17
  'CI_JOB_URL' => :ci_job_url,
18
+ 'CI_JOB_STATUS' => :ci_job_status,
18
19
  'CI_PIPELINE_ID' => :ci_pipeline_id,
19
20
  'CI_PIPELINE_NAME' => :ci_pipeline_name,
20
21
  'CI_PIPELINE_URL' => :ci_pipeline_url,
21
22
  'CI_PROJECT_ID' => :ci_project_id,
22
23
  'CI_PROJECT_NAME' => :ci_project_name,
23
24
  'CI_PROJECT_PATH' => :ci_project_path,
25
+ 'CI_PIPELINE_CREATED_AT' => :ci_pipeline_created_at,
24
26
  'DEPLOY_VERSION' => :deploy_version,
25
27
  'GITLAB_QA_ISSUE_URL' => :qa_issue_url,
26
- 'QA_GITLAB_CI_TOKEN' => :gitlab_ci_token,
27
- 'SLACK_QA_CHANNEL' => :slack_qa_channel
28
+ 'QA_GITLAB_CI_TOKEN' => :gitlab_ci_token
28
29
  }.freeze
29
30
 
30
31
  ENV_VARIABLES.each do |env_name, method_name|
@@ -61,6 +62,10 @@ module GitlabQuality
61
62
  env_var_value_if_defined('GITLAB_GRAPHQL_API_BASE')
62
63
  end
63
64
 
65
+ def slack_alerts_channel
66
+ env_var_value_if_defined('SLACK_ALERTS_CHANNEL') || 'C09HQ5BN07J' # test-tooling-alerts channel ID
67
+ end
68
+
64
69
  def pipeline_from_project_name
65
70
  %w[gitlab gitaly].any? { |str| ci_project_name.to_s.start_with?(str) } ? default_branch : ci_project_name
66
71
  end
@@ -111,12 +116,12 @@ module GitlabQuality
111
116
  end
112
117
 
113
118
  def env_var_value_if_defined(variable)
114
- return ENV.fetch(variable) if env_var_value_valid?(variable)
119
+ ENV.fetch(variable) if env_var_value_valid?(variable)
115
120
  end
116
121
 
117
122
  def env_var_name_if_defined(variable)
118
123
  # Pass through the variables if they are defined and not empty in the environment
119
- return "$#{variable}" if env_var_value_valid?(variable)
124
+ "$#{variable}" if env_var_value_valid?(variable)
120
125
  end
121
126
  end
122
127
  end
@@ -1,15 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module GitlabQuality
4
6
  module TestTooling
5
7
  module Slack
6
8
  class PostToSlack
7
- def initialize(slack_webhook_url:, channel:, message:, username:, icon_emoji:)
9
+ MAX_PATTERN_MESSAGE_LENGTH = 150
10
+ MAX_GROUPED_FAILURES_TO_DISPLAY = 10
11
+
12
+ def initialize(slack_webhook_url:, channel:, message:, username:, icon_emoji:, environment_issues_file: nil)
8
13
  @slack_webhook_url = slack_webhook_url
9
14
  @channel = channel
10
15
  @message = message
11
16
  @username = username
12
17
  @icon_emoji = icon_emoji
18
+ @environment_issues_file = environment_issues_file
13
19
  end
14
20
 
15
21
  def invoke!
@@ -17,7 +23,7 @@ module GitlabQuality
17
23
  params['channel'] = channel
18
24
  params['username'] = username
19
25
  params['icon_emoji'] = icon_emoji
20
- params['text'] = message
26
+ params['text'] = build_message
21
27
 
22
28
  Support::HttpRequest.make_http_request(
23
29
  method: 'post',
@@ -29,7 +35,101 @@ module GitlabQuality
29
35
 
30
36
  private
31
37
 
32
- attr_reader :slack_webhook_url, :channel, :message, :username, :icon_emoji
38
+ attr_reader :slack_webhook_url, :channel, :message, :username, :icon_emoji, :environment_issues_file
39
+
40
+ def build_message
41
+ messages = []
42
+ messages << message if message && !message.empty?
43
+ messages << format_environment_issues if environment_issues_file && File.exist?(environment_issues_file)
44
+
45
+ messages.join("\n\n")
46
+ end
47
+
48
+ def format_environment_issues
49
+ issues_data = JSON.parse(File.read(environment_issues_file))
50
+ return nil if issues_data.nil? || issues_data['grouped_failures'].empty?
51
+
52
+ build_slack_message(issues_data)
53
+ rescue JSON::ParserError => e
54
+ ":x: Error parsing environment issues file: #{e.message}"
55
+ rescue StandardError => e
56
+ ":x: Error formatting environment issues: #{e.message}"
57
+ end
58
+
59
+ def format_single_environment_issue(failure)
60
+ pattern_title = pattern_display_name(failure['pattern_name'])
61
+
62
+ issue_text = build_issue_header(pattern_title, failure)
63
+ issue_text + build_job_info(failure)
64
+ end
65
+
66
+ def pattern_display_name(pattern_name)
67
+ case pattern_name&.downcase
68
+ when /http_500/
69
+ "HTTP 500 Internal Server Errors"
70
+ when /http_400/
71
+ "HTTP 400 Bad Request Errors"
72
+ when /http_503/
73
+ "HTTP 503 Service Unavailable"
74
+ when /timeout/
75
+ "Timeout Errors"
76
+ when /git_rpc|repository/
77
+ "Git/Repository Errors"
78
+ else
79
+ "#{pattern_name&.humanize || 'Unknown'} Errors"
80
+ end
81
+ end
82
+
83
+ def build_slack_message(issues_data)
84
+ header = ":warning: *Environment Issues Detected*\n"
85
+
86
+ issue_messages = format_issue_messages(issues_data['grouped_failures'])
87
+ truncation_note = build_truncation_note(issues_data['grouped_failures'].size)
88
+ summary = build_summary_text(issues_data['summary'])
89
+
90
+ header + issue_messages + truncation_note + summary
91
+ end
92
+
93
+ def format_issue_messages(grouped_failures)
94
+ failures_to_show = grouped_failures.first(MAX_GROUPED_FAILURES_TO_DISPLAY)
95
+ failures_to_show.map { |failure| format_single_environment_issue(failure) }.join("\n\n")
96
+ end
97
+
98
+ def build_truncation_note(total_failures)
99
+ return "" unless total_failures > MAX_GROUPED_FAILURES_TO_DISPLAY
100
+
101
+ "\n_... and #{total_failures - MAX_GROUPED_FAILURES_TO_DISPLAY} more environment issue(s)_"
102
+ end
103
+
104
+ def build_issue_header(pattern_title, failure)
105
+ <<~TEXT
106
+ *#{pattern_title}*
107
+ • Affected tests: #{failure['failure_count']}
108
+ • Pattern: `#{truncate_message(failure['normalized_message'])}`
109
+ TEXT
110
+ end
111
+
112
+ def build_job_info(failure)
113
+ return "" unless failure['failures']&.any?
114
+
115
+ job_urls = failure['failures'].filter_map { |f| f['ci_job_url'] }.uniq
116
+ job_urls.any? ? "• Jobs affected: #{job_urls.size}\n" : ""
117
+ end
118
+
119
+ def build_summary_text(summary)
120
+ <<~TEXT
121
+
122
+ *Summary:* #{summary['grouped_issues']} environment issue(s) affecting #{summary['total_grouped_failures']} test(s)
123
+
124
+ _Note: Future improvements will include direct GitLab issue links and enhanced filtering._
125
+ _Track progress: https://gitlab.com/groups/gitlab-org/quality/quality-engineering/-/epics/168_
126
+ TEXT
127
+ end
128
+
129
+ def truncate_message(message)
130
+ text = message.to_s
131
+ text.length > MAX_PATTERN_MESSAGE_LENGTH ? "#{text[0..MAX_PATTERN_MESSAGE_LENGTH]}..." : text
132
+ end
33
133
  end
34
134
  end
35
135
  end
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class ApiLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/api_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class ApplicationLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/application_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class ExceptionLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/exceptions_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -7,7 +7,7 @@ module GitlabQuality
7
7
  module Rails
8
8
  class GraphqlLogFinder < JsonLogFinder
9
9
  def initialize(base_path, file_path = 'gitlab-rails/graphql_json.log')
10
- super(base_path, file_path)
10
+ super
11
11
  end
12
12
 
13
13
  def new_log(data)
@@ -8,9 +8,7 @@ module GitlabQuality
8
8
  class TestMetaUpdater
9
9
  include TestTooling::Concerns::FindSetDri
10
10
 
11
- attr_reader :project, :ref, :report_issue, :processed_commits
12
-
13
- TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
11
+ attr_reader :project, :ref, :report_issue, :processed_commits, :token, :specs_file, :dry_run, :processor
14
12
 
15
13
  def initialize(token:, project:, specs_file:, processor:, ref: 'master', dry_run: false)
16
14
  @specs_file = specs_file
@@ -269,12 +267,15 @@ module GitlabQuality
269
267
  # Fetch the id for the dri of the product group and stage
270
268
  # The first item returned is the id of the assignee and the second item is the handle
271
269
  #
272
- # @param [String] product_group
270
+ # @param [Hash] test object
273
271
  # @param [String] devops_stage
272
+ # @param [String] section
274
273
  # @return [Array<Integer, String>]
275
- def fetch_dri_id(product_group, devops_stage, section)
276
- assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || test_dri(product_group, devops_stage, section)
274
+ def fetch_dri_id(test, devops_stage, section)
275
+ product_group = determine_product_group(test)
276
+ return unless product_group
277
277
 
278
+ assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || test_dri(product_group, devops_stage, section)
278
279
  [user_id_for_username(assignee_handle), assignee_handle]
279
280
  end
280
281
 
@@ -291,7 +292,7 @@ module GitlabQuality
291
292
  # @param [String] message the message to post
292
293
  # @return [HTTP::Response]
293
294
  def post_message_on_slack(message)
294
- channel = ENV.fetch('SLACK_QA_CHANNEL', nil) || TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID
295
+ channel = Runtime::Env.slack_alerts_channel
295
296
  slack_options = {
296
297
  slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
297
298
  channel: channel,
@@ -335,6 +336,16 @@ module GitlabQuality
335
336
  label ? %(/label ~"#{label}") : ''
336
337
  end
337
338
 
339
+ # Infers the group label from the provided feature category
340
+ #
341
+ # @param [String] feature_category feature category
342
+ # @return [String]
343
+ def label_from_feature_category(feature_category)
344
+ labels = labels_inference.infer_labels_from_feature_category(feature_category)
345
+ group_label = labels.find { |label| label.start_with?('group::') }
346
+ group_label ? %(/label ~"#{group_label}") : ''
347
+ end
348
+
338
349
  # Returns the link to the Grafana dashboard for single spec metrics
339
350
  #
340
351
  # @param [String] example_name the full example name
@@ -344,10 +355,6 @@ module GitlabQuality
344
355
  base_url + CGI.escape(example_name)
345
356
  end
346
357
 
347
- private
348
-
349
- attr_reader :token, :specs_file, :dry_run, :processor
350
-
351
358
  # Returns any test description string within single or double quotes
352
359
  #
353
360
  # @param [String] line the line to check for any quoted string
@@ -380,6 +387,27 @@ module GitlabQuality
380
387
  def labels_inference
381
388
  @labels_inference ||= GitlabQuality::TestTooling::LabelsInference.new
382
389
  end
390
+
391
+ private
392
+
393
+ def determine_product_group(test)
394
+ return map_feature_category_to_product_group(test) if has_feature_category?(test)
395
+ return test.product_group if has_product_group?(test)
396
+
397
+ nil
398
+ end
399
+
400
+ def has_feature_category?(test)
401
+ test.respond_to?(:feature_category) && test.feature_category?
402
+ end
403
+
404
+ def has_product_group?(test)
405
+ test.respond_to?(:product_group) && test.product_group?
406
+ end
407
+
408
+ def map_feature_category_to_product_group(test)
409
+ labels_inference.product_group_from_feature_category(test.feature_category)
410
+ end
383
411
  end
384
412
  end
385
413
  end