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.
- 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 +113 -0
- data/lib/gitlab_quality/test_tooling/click_house/client.rb +111 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +77 -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 +62 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +109 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +73 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +82 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +91 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb +43 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_map.rb +93 -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 +125 -80
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +95 -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 +82 -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
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'] =
|
|
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
|
|
@@ -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 [
|
|
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(
|
|
276
|
-
|
|
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 =
|
|
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
|