gitlab_quality-test_tooling 2.8.0 → 2.10.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 (25) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +2 -1
  3. data/Gemfile.lock +10 -2
  4. data/README.md +27 -3
  5. data/exe/failed-test-issues +53 -8
  6. data/exe/feature-readiness-check +61 -0
  7. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/issue_concern.rb +34 -0
  8. data/lib/gitlab_quality/test_tooling/feature_readiness/concerns/work_item_concern.rb +114 -0
  9. data/lib/gitlab_quality/test_tooling/feature_readiness/operational_readiness_check.rb +82 -0
  10. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_graphql_client.rb +54 -0
  11. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +32 -0
  12. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +3 -3
  13. data/lib/gitlab_quality/test_tooling/gitlab_client/labels_client.rb +13 -0
  14. data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_client.rb +240 -0
  15. data/lib/gitlab_quality/test_tooling/gitlab_client/work_items_dry_client.rb +25 -0
  16. data/lib/gitlab_quality/test_tooling/labels_inference.rb +4 -0
  17. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +6 -6
  18. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +120 -20
  19. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +1 -1
  20. data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +1 -1
  21. data/lib/gitlab_quality/test_tooling/runtime/env.rb +11 -6
  22. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/support/gcs_tools.rb +18 -5
  23. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +2 -1
  24. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  25. metadata +29 -6
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module GitlabClient
6
+ # The GitLab client is used for API access: https://github.com/NARKOZ/gitlab
7
+ class WorkItemsClient < GitlabGraphqlClient
8
+ def work_item(workitem_iid: nil)
9
+ query = <<~GQL
10
+ query {
11
+ namespace(fullPath: "#{group}") {
12
+ workItem(iid: "#{workitem_iid}") {
13
+ #{work_item_fields}
14
+ #{work_item_widgets}
15
+ }
16
+ }
17
+ }
18
+ GQL
19
+ post(query)[:workItem]
20
+ end
21
+
22
+ def group_work_items(labels: [], cursor: '', state: 'opened', created_after: nil, extras: [])
23
+ query = <<~GQL
24
+ query {
25
+ group(fullPath: "#{group}") {
26
+ workItems(after: "#{cursor}",#{created_after ? " createdAfter: \"#{created_after}\"," : ''} labelName: [#{labels.map { |label| "\"#{label}\"" }.join(', ')}], state: #{state}) {
27
+ nodes {
28
+ #{construct_extras(extras)}
29
+ }
30
+ pageInfo {
31
+ hasNextPage
32
+ endCursor
33
+ }
34
+ }
35
+ }
36
+ }
37
+ GQL
38
+ begin
39
+ post(query)[:workItems]
40
+ rescue StandardError => e
41
+ puts "Error: #{e}"
42
+ end
43
+ end
44
+
45
+ def create_discussion(id:, note:)
46
+ post(
47
+ <<~GQL
48
+ mutation CreateDiscussion {
49
+ createDiscussion(input: {noteableId: "#{id}", body: "#{note}"}) {
50
+ clientMutationId
51
+ errors
52
+ note {
53
+ #{note_fields}
54
+ }
55
+ }
56
+ }
57
+ GQL
58
+ )
59
+ end
60
+
61
+ def create_discussion_note(work_item_id:, discussion_id:, text:)
62
+ query = <<~GQL
63
+ mutation CreateNote {
64
+ createNote(input: { discussionId: "#{discussion_id}", noteableId: "#{work_item_id}", body: "#{text}" }) {
65
+ clientMutationId
66
+ errors
67
+ note {
68
+ #{note_fields}
69
+ }
70
+ }
71
+ }
72
+ GQL
73
+ post(query)
74
+ end
75
+
76
+ def create_linked_items(work_item_id:, item_ids:, link_type:)
77
+ query = <<~GQL
78
+ mutation WorkItemAddLinkedItems {
79
+ workItemAddLinkedItems(
80
+ input: { id: "#{work_item_id}", workItemsIds: [#{item_ids.map { |id| "\"#{id}\"" }.join(', ')}], linkType: #{link_type} }
81
+ ) {
82
+ clientMutationId
83
+ }
84
+ }
85
+ GQL
86
+ post(query)
87
+ end
88
+
89
+ def add_labels(work_item_id:, label_ids:)
90
+ query =
91
+ <<~GQL
92
+ mutation WorkItemUpdate {
93
+ workItemUpdate(input: { id: "#{work_item_id}", labelsWidget: { addLabelIds: [#{label_ids.map { |id| "\"#{id}\"" }.join(', ')}] } }) {
94
+ clientMutationId
95
+ errors
96
+ }
97
+ }
98
+ GQL
99
+ post(query)
100
+ end
101
+
102
+ def paginated_call(method_name, args)
103
+ results = []
104
+
105
+ # Check if the method exists
106
+ raise ArgumentError, "Unknown method: #{method_name}" unless respond_to?(method_name, true)
107
+
108
+ method_obj = method(method_name)
109
+
110
+ loop do
111
+ # Call the method directly using the method object
112
+ response = method_obj.call(**args)
113
+
114
+ break unless response
115
+
116
+ results += response[:nodes]
117
+ break unless response[:pageInfo][:hasNextPage]
118
+
119
+ args[:cursor] = response[:pageInfo][:endCursor]
120
+ end
121
+
122
+ results
123
+ end
124
+
125
+ private
126
+
127
+ attr_reader :group
128
+
129
+ def construct_extras(extras)
130
+ extras_string = ""
131
+ extras.each do |extra|
132
+ next unless respond_to?(extra, true)
133
+
134
+ method_obj = method(extra)
135
+ extras_string += method_obj.call
136
+ extras_string += "\n"
137
+ end
138
+ extras_string
139
+ end
140
+
141
+ # https://docs.gitlab.com/api/graphql/reference/#workitem
142
+ def work_item_fields
143
+ <<~GQL
144
+ author {
145
+ username
146
+ }
147
+ createdAt
148
+ iid
149
+ id
150
+ project {
151
+ fullPath
152
+ id
153
+ }
154
+ state
155
+ title
156
+ webUrl
157
+ workItemType {
158
+ name
159
+ }
160
+ GQL
161
+ end
162
+
163
+ def work_item_widgets
164
+ <<~GQL
165
+ widgets(onlyTypes: [LINKED_ITEMS, NOTES, LABELS, HIERARCHY]) {
166
+ ... on WorkItemWidgetNotes {
167
+ discussions(filter: ONLY_COMMENTS) {
168
+ nodes {
169
+ notes {
170
+ nodes {
171
+ #{note_fields}
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ ... on WorkItemWidgetLinkedItems {
178
+ linkedItems {
179
+ nodes {
180
+ linkType
181
+ workItem {
182
+ #{work_item_fields}
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ ... on WorkItemWidgetLabels{
189
+ labels{
190
+ nodes{
191
+ title
192
+ }
193
+ }
194
+ }
195
+
196
+ ... on WorkItemWidgetHierarchy {
197
+ children {
198
+ nodes{
199
+ #{work_item_fields}
200
+ }
201
+ }
202
+ }
203
+ }
204
+ GQL
205
+ end
206
+
207
+ # https://docs.gitlab.com/api/graphql/reference/#note
208
+ def note_fields
209
+ <<~GQL
210
+ author {
211
+ username
212
+ }
213
+ awardEmoji {
214
+ nodes {
215
+ #{emoji_fields}
216
+ }
217
+ }
218
+ body
219
+ id
220
+ url
221
+ discussion {
222
+ id
223
+ }
224
+ GQL
225
+ end
226
+
227
+ # https://docs.gitlab.com/api/graphql/reference/#awardemoji
228
+ def emoji_fields
229
+ <<~GQL
230
+ emoji
231
+ name
232
+ user {
233
+ username
234
+ }
235
+ GQL
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module GitlabClient
6
+ class WorkItemsDryClient < WorkItemsClient
7
+ def create_discussion(id:, note:)
8
+ puts "The following discussion would have been posted on #{project}##{id} epic:\n\n #{note}"
9
+ end
10
+
11
+ def create_discussion_note(work_item_id:, discussion_id:, text:)
12
+ puts "The following discussion note would have been posted on #{project}##{work_item_id} (discussion #{discussion_id}) text:\n\n #{text}"
13
+ end
14
+
15
+ def create_linked_items(work_item_id:, item_ids:, link_type: 'BLOCKED_BY')
16
+ puts "The following items would have been linked on #{project}##{work_item_id} (link type: #{link_type}) item_ids: #{item_ids.join(', ')}"
17
+ end
18
+
19
+ def add_labels(work_item_id:, label_ids:)
20
+ puts "The following labels would have been added to #{project}##{work_item_id} work item: #{label_ids.join(', ')}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -22,6 +22,10 @@ module GitlabQuality
22
22
  ].compact.to_set
23
23
  end
24
24
 
25
+ def product_group_from_feature_category(feature_category)
26
+ categories_mapping.dig(feature_category, 'group')
27
+ end
28
+
25
29
  private
26
30
 
27
31
  def categories_mapping
@@ -190,7 +190,7 @@ module GitlabQuality
190
190
  note_stacktrace = sanitize_stacktrace(stacktrace: note.body, regex: ISSUE_STACKTRACE_REGEX)
191
191
  return note_stacktrace if note_stacktrace
192
192
 
193
- puts " => [DEBUG] Stacktrace couldn't be found for #{issue.web_url}#note_#{note.id}!"
193
+ Runtime::Logger.debug " => Stacktrace couldn't be found for #{issue.web_url}#note_#{note.id}!"
194
194
  end
195
195
 
196
196
  def sanitize_stacktrace(stacktrace:, regex:)
@@ -199,7 +199,7 @@ module GitlabQuality
199
199
  if stacktrace_match
200
200
  stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
201
201
  else
202
- puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex})!"
202
+ Runtime::Logger.debug " => Stacktrace doesn't match the regex (#{regex})!"
203
203
  end
204
204
  end
205
205
 
@@ -209,7 +209,7 @@ module GitlabQuality
209
209
  stack_trace_comparator = StackTraceComparator.new(test_stacktrace, note_stacktrace)
210
210
 
211
211
  if stack_trace_comparator.lower_or_equal_to_diff_ratio?(max_diff_ratio)
212
- puts " => [DEBUG] Note #{issue.web_url}#note_#{note.id} has an acceptable diff ratio of #{stack_trace_comparator.diff_percent}%."
212
+ Runtime::Logger.debug " => Note #{issue.web_url}#note_#{note.id} has an acceptable diff ratio of #{stack_trace_comparator.diff_percent}%."
213
213
  # The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
214
214
  # This leads to a `TypeError Exception: no implicit conversion of Hash into Integer` error, so we convert the object to a hash before using it as a Hash key.
215
215
  # See:
@@ -217,9 +217,9 @@ module GitlabQuality
217
217
  # - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
218
218
  stack_trace_comparator.diff_ratio
219
219
  else
220
- puts " => [DEBUG] Found note #{issue.web_url}#note_#{note.id} but stacktraces are too different (#{stack_trace_comparator.diff_percent}%).\n"
221
- puts " => [DEBUG] Issue stacktrace:\n----------------\n#{note_stacktrace}\n----------------\n"
222
- puts " => [DEBUG] Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
220
+ Runtime::Logger.debug " => Found note #{issue.web_url}#note_#{note.id} but stacktraces are too different (#{stack_trace_comparator.diff_percent}%).\n"
221
+ Runtime::Logger.debug " => Issue stacktrace:\n----------------\n#{note_stacktrace}\n----------------\n"
222
+ Runtime::Logger.debug " => Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
223
223
  end
224
224
  end
225
225
  end
@@ -15,13 +15,29 @@ module GitlabQuality
15
15
  class HealthProblemReporter < ReportAsIssue
16
16
  include Concerns::GroupAndCategoryLabels
17
17
  include Concerns::IssueReports
18
+ include TestMetricsExporter::Support::GcsTools
18
19
 
19
20
  BASE_SEARCH_LABELS = ['test'].freeze
20
21
  FOUND_IN_MR_LABEL = '~"found:in MR"'
21
22
  FOUND_IN_MASTER_LABEL = '~"found:master"'
22
23
 
23
- def initialize(input_files: [], **kwargs)
24
- super(input_files: input_files, **kwargs)
24
+ def initialize(
25
+ input_files: [],
26
+ dry_run: false,
27
+ issue_update_enabled: true,
28
+ gcs_enabled: false,
29
+ gcs_project_id: nil,
30
+ gcs_bucket: nil,
31
+ gcs_credentials: nil,
32
+ **kwargs)
33
+ super(input_files: input_files, dry_run: dry_run, **kwargs)
34
+
35
+ @dry_run = dry_run
36
+ @issue_update_enabled = issue_update_enabled
37
+ @gcs_enabled = gcs_enabled
38
+ @gcs_project_id = gcs_project_id
39
+ @gcs_bucket = gcs_bucket
40
+ @gcs_credentials = gcs_credentials
25
41
  end
26
42
 
27
43
  def most_recent_report_date_for_issue(issue_iid:)
@@ -33,6 +49,8 @@ module GitlabQuality
33
49
 
34
50
  private
35
51
 
52
+ attr_reader :dry_run, :issue_update_enabled, :gcs_enabled, :gcs_project_id, :gcs_bucket, :gcs_credentials
53
+
36
54
  def problem_type
37
55
  'unhealthy'
38
56
  end
@@ -70,43 +88,89 @@ module GitlabQuality
70
88
  end
71
89
 
72
90
  def run!
73
- puts "Reporting tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
91
+ Runtime::Logger.info "issue creation/update is disabled." unless issue_update_enabled
92
+ Runtime::Logger.info "push to GCS is disabled." unless gcs_enabled
93
+ return unless test_reporting_enabled?
74
94
 
95
+ Runtime::Logger.info "Reporting tests in `#{files.join(',')}`."
75
96
  TestResults::Builder.new(file_glob: files, token: token, project: project).test_results_per_file do |test_results|
76
- puts "=> Processing #{test_results.count} tests in #{test_results.path}"
97
+ Runtime::Logger.info "=> Processing #{test_results.count} tests in #{test_results.path}"
77
98
 
78
99
  process_test_results(test_results)
79
100
  end
80
101
  end
81
102
 
82
103
  def process_test_results(test_results)
83
- reported_test_count = 0
104
+ applicable_tests = test_results.select { |test| test_is_applicable?(test) }
105
+ return if applicable_tests.empty?
106
+
107
+ process_issues_for_tests(applicable_tests) if issue_update_enabled
84
108
 
85
- test_results.each do |test|
86
- next unless test_is_applicable?(test)
109
+ if gcs_enabled
110
+ tests_data = build_tests_data(applicable_tests)
111
+ test_results_filename = File.basename(test_results.path)
112
+
113
+ push_test_to_gcs(tests_data, test_results_filename)
114
+ end
87
115
 
88
- puts " => Reporting #{problem_type} test '#{test.name}'..."
116
+ Runtime::Logger.info " => Reported #{applicable_tests.size} #{problem_type} tests."
117
+ end
118
+
119
+ def process_issues_for_tests(tests)
120
+ tests.each do |test|
121
+ Runtime::Logger.info " => Processing issues for #{problem_type} test '#{test.name}'..."
122
+ issues = existing_test_health_issues_for_test(test)
123
+ create_or_update_test_health_issues(issues, test)
124
+ end
125
+ end
89
126
 
90
- issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: search_labels)
127
+ def existing_test_health_issues_for_test(test)
128
+ find_issues_by_hash(test_hash(test), state: 'opened', labels: search_labels)
129
+ end
91
130
 
92
- if issues.empty?
93
- issues << create_issue(test)
94
- else
95
- # Keep issues description up-to-date
96
- update_issues(issues, test)
97
- end
131
+ def build_tests_data(tests)
132
+ tests.map do |test|
133
+ Runtime::Logger.info " => Building data for #{problem_type} test '#{test.name}'..."
134
+ issues = existing_test_health_issues_for_test(test)
135
+ test_datum(test, issues)
136
+ end
137
+ end
98
138
 
99
- update_reports(issues, test)
100
- collect_issues(test, issues)
101
- reported_test_count += 1
139
+ def create_or_update_test_health_issues(issues, test)
140
+ if issues.empty?
141
+ issues << create_issue(test)
142
+ else
143
+ # Keep issues description up-to-date
144
+ update_issues(issues, test)
102
145
  end
103
146
 
104
- puts " => Reported #{reported_test_count} #{problem_type} tests."
147
+ update_reports(issues, test)
148
+ collect_issues(test, issues)
149
+ end
150
+
151
+ def push_test_to_gcs(tests_data, test_results_filename)
152
+ Runtime::Logger.info "will push the test data to GCS"
153
+
154
+ gcs_client(project_id: gcs_project_id, credentials: gcs_credentials, dry_run: dry_run).put_object(
155
+ gcs_bucket,
156
+ gcs_metrics_file_name(test_results_filename),
157
+ tests_data.to_json,
158
+ force: true,
159
+ content_type: 'application/json'
160
+ )
161
+
162
+ Runtime::Logger.info "Successfully pushed the #{gcs_metrics_file_name(test_results_filename)} file to the GCS bucket."
163
+ end
164
+
165
+ def gcs_metrics_file_name(test_results_filename)
166
+ today = Time.now.strftime('%Y-%m-%d')
167
+
168
+ "#{today}-#{test_results_filename}"
105
169
  end
106
170
 
107
171
  def update_reports(issues, test)
108
172
  issues.each do |issue|
109
- puts " => Reporting #{problem_type} test to existing issue: #{issue.web_url}"
173
+ Runtime::Logger.info " => Reporting #{problem_type} test to existing issue: #{issue.web_url}"
110
174
  add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
111
175
  end
112
176
  end
@@ -162,6 +226,42 @@ module GitlabQuality
162
226
  end
163
227
  end
164
228
 
229
+ # We report a test if we have issue update and/or push to GCS enabled.
230
+ def test_reporting_enabled?
231
+ issue_update_enabled || gcs_enabled
232
+ end
233
+
234
+ # rubocop:disable Metrics/AbcSize
235
+ def test_datum(test, issues)
236
+ feature_category = test.feature_category.to_s.strip.empty? ? nil : test.feature_category
237
+ product_group = test.product_group.to_s.strip.empty? ? nil : test.product_group
238
+
239
+ if !product_group && feature_category
240
+ labels_inference = GitlabQuality::TestTooling::LabelsInference
241
+ product_group = labels_inference.new.product_group_from_feature_category(feature_category)
242
+ end
243
+
244
+ {
245
+ feature_category: feature_category,
246
+ created_at: Time.now.utc.iso8601,
247
+ description: test.name,
248
+ filename: test.file,
249
+ gitlab_project_id: Runtime::Env.ci_project_id,
250
+ product_group: product_group,
251
+ id: test.example_id,
252
+ hash: test_hash(test),
253
+ issue_url: issues.first&.web_url,
254
+ job_id: Runtime::Env.ci_job_id,
255
+ job_web_url: test.ci_job_url,
256
+ pipeline_id: Runtime::Env.ci_pipeline_id,
257
+ pipeline_ref: Runtime::Env.ci_commit_ref_name,
258
+ pipeline_web_url: Runtime::Env.ci_pipeline_url,
259
+ stacktrace: test.full_stacktrace,
260
+ test_level: test.level
261
+ }
262
+ end
263
+ # rubocop:enable Metrics/AbcSize
264
+
165
265
  def found_label
166
266
  if ENV.key?('CI_MERGE_REQUEST_IID')
167
267
  FOUND_IN_MR_LABEL
@@ -17,7 +17,7 @@ module GitlabQuality
17
17
  include Concerns::GroupAndCategoryLabels
18
18
 
19
19
  NEW_ISSUE_LABELS = Set.new([
20
- 'test', 'automation::bot-authored', 'type::maintenance', 'maintenance::performance',
20
+ 'test', 'automation:bot-authored', 'type::maintenance', 'maintenance::performance',
21
21
  'priority::3', 'severity::3', 'knapsack_report'
22
22
  ]).freeze
23
23
  SEARCH_LABELS = %w[test maintenance::performance knapsack_report].freeze
@@ -6,7 +6,7 @@ module GitlabQuality
6
6
  module TestTooling
7
7
  module Report
8
8
  class PrepareStageReports
9
- EXTRACT_STAGE_FROM_TEST_FILE_REGEX = %r{(?:api|browser_ui)/(?:[0-9]+_)?(?<stage>[_\w]+)/}i
9
+ EXTRACT_STAGE_FROM_TEST_FILE_REGEX = %r{(?:api|browser_ui)/(?:[0-9]+_)?(?<stage>[\w]+)/}i
10
10
 
11
11
  def initialize(input_files:)
12
12
  @input_files = input_files
@@ -11,19 +11,20 @@ module GitlabQuality
11
11
  using Rainbow
12
12
 
13
13
  ENV_VARIABLES = {
14
- 'GITLAB_QA_ISSUE_URL' => :qa_issue_url,
15
14
  'CI_COMMIT_REF_NAME' => :ci_commit_ref_name,
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_PROJECT_ID' => :ci_project_id,
19
- 'CI_PROJECT_NAME' => :ci_project_name,
20
- 'CI_PROJECT_PATH' => :ci_project_path,
21
18
  'CI_PIPELINE_ID' => :ci_pipeline_id,
22
19
  'CI_PIPELINE_NAME' => :ci_pipeline_name,
23
20
  'CI_PIPELINE_URL' => :ci_pipeline_url,
24
- 'SLACK_QA_CHANNEL' => :slack_qa_channel,
21
+ 'CI_PROJECT_ID' => :ci_project_id,
22
+ 'CI_PROJECT_NAME' => :ci_project_name,
23
+ 'CI_PROJECT_PATH' => :ci_project_path,
25
24
  'DEPLOY_VERSION' => :deploy_version,
26
- 'QA_GITLAB_CI_TOKEN' => :gitlab_ci_token
25
+ 'GITLAB_QA_ISSUE_URL' => :qa_issue_url,
26
+ 'QA_GITLAB_CI_TOKEN' => :gitlab_ci_token,
27
+ 'SLACK_QA_CHANNEL' => :slack_qa_channel
27
28
  }.freeze
28
29
 
29
30
  ENV_VARIABLES.each do |env_name, method_name|
@@ -56,6 +57,10 @@ module GitlabQuality
56
57
  env_var_value_if_defined('GITLAB_API_BASE') || ci_api_v4_url
57
58
  end
58
59
 
60
+ def gitlab_graphql_api_base
61
+ env_var_value_if_defined('GITLAB_GRAPHQL_API_BASE')
62
+ end
63
+
59
64
  def pipeline_from_project_name
60
65
  %w[gitlab gitaly].any? { |str| ci_project_name.to_s.start_with?(str) } ? default_branch : ci_project_name
61
66
  end
@@ -12,11 +12,15 @@ module GitlabQuality
12
12
  # @param project_id [String]
13
13
  # @param credentials [String]
14
14
  # @return [Fog::Storage::Google]
15
- def gcs_client(project_id:, credentials:)
16
- Fog::Storage::Google.new(
17
- google_project: project_id || raise("Missing Google project_id"),
18
- **gcs_creds(credentials)
19
- )
15
+ def gcs_client(project_id:, credentials:, dry_run: false)
16
+ if dry_run
17
+ GCSMockClient.new
18
+ else
19
+ Fog::Storage::Google.new(
20
+ google_project: project_id || raise("Missing Google project_id"),
21
+ **gcs_creds(credentials)
22
+ )
23
+ end
20
24
  end
21
25
 
22
26
  # GCS Credentials
@@ -29,6 +33,15 @@ module GitlabQuality
29
33
 
30
34
  { google_json_key_string: json_key }
31
35
  end
36
+
37
+ class GCSMockClient
38
+ def initialize(*_args, **_kwargs); end
39
+
40
+ def put_object(gcs_bucket, filename, data, **_kwargs)
41
+ Runtime::Logger.info "The #{filename} file would have been pushed to the #{gcs_bucket} bucket, with this content:"
42
+ Runtime::Logger.info data
43
+ end
44
+ end
32
45
  end
33
46
  end
34
47
  end
@@ -13,7 +13,8 @@ module GitlabQuality
13
13
  "could not be found (502)",
14
14
  "Error reference number: 502",
15
15
  "(502): `GitLab is not responding`",
16
- "<head><title>502 Bad Gateway</title></head>"
16
+ "<head><title>502 Bad Gateway</title></head>",
17
+ "14:connections to all backends failing"
17
18
  ].freeze
18
19
 
19
20
  SHARED_EXAMPLES_CALLERS = %w[include_examples it_behaves_like].freeze
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "2.8.0"
5
+ VERSION = "2.10.0"
6
6
  end
7
7
  end