gitlab-qa 5.14.1 → 6.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.
@@ -4,8 +4,9 @@ module Gitlab
4
4
  class Command
5
5
  attr_reader :args
6
6
 
7
- def initialize(cmd = nil)
7
+ def initialize(cmd = nil, mask_secrets: nil)
8
8
  @args = Array(cmd)
9
+ @mask_secrets = Array(mask_secrets)
9
10
  end
10
11
 
11
12
  def <<(*args)
@@ -28,6 +29,17 @@ module Gitlab
28
29
  "docker #{@args.join(' ')}"
29
30
  end
30
31
 
32
+ # Returns a masked string form of a Command
33
+ #
34
+ # @example
35
+ # Command.new('a docker command', mask_secrets: 'command').mask_secrets #=> 'a docker *****'
36
+ # Command.new('a docker command', mask_secrets: %w[docker command]).mask_secrets #=> 'a ***** *****'
37
+ #
38
+ # @return [String] The masked command string
39
+ def mask_secrets
40
+ @mask_secrets.each_with_object(to_s) { |secret, s| s.gsub!(secret, '*****') }
41
+ end
42
+
31
43
  def ==(other)
32
44
  to_s == other.to_s
33
45
  end
@@ -36,8 +48,8 @@ module Gitlab
36
48
  Docker::Shellout.new(self).execute!(&block)
37
49
  end
38
50
 
39
- def self.execute(cmd, &block)
40
- new(cmd).execute!(&block)
51
+ def self.execute(cmd, mask_secrets: nil, &block)
52
+ new(cmd, mask_secrets: mask_secrets).execute!(&block)
41
53
  end
42
54
  end
43
55
  end
@@ -10,7 +10,7 @@ module Gitlab
10
10
  end
11
11
 
12
12
  def login(username:, password:, registry:)
13
- Docker::Command.execute(%(login --username "#{username}" --password "#{password}" #{registry}))
13
+ Docker::Command.execute(%(login --username "#{username}" --password "#{password}" #{registry}), mask_secrets: password)
14
14
  end
15
15
 
16
16
  def pull(image, tag)
@@ -82,6 +82,10 @@ module Gitlab
82
82
  def running?(name)
83
83
  Docker::Command.execute("ps -f name=#{name}").include?(name)
84
84
  end
85
+
86
+ def ps(name = nil)
87
+ Docker::Command.execute(['ps', name].compact.join(' '))
88
+ end
85
89
  end
86
90
  end
87
91
  end
@@ -10,7 +10,7 @@ module Gitlab
10
10
  @command = command
11
11
  @output = []
12
12
 
13
- puts "Docker shell command: `#{@command}`"
13
+ puts "Docker shell command: `#{@command.mask_secrets}`"
14
14
  end
15
15
 
16
16
  def execute!
@@ -28,7 +28,7 @@ module Gitlab
28
28
  end
29
29
 
30
30
  if wait.value.exited? && wait.value.exitstatus.nonzero?
31
- raise StatusError, "Docker command `#{@command}` failed!"
31
+ raise StatusError, "Docker command `#{@command.mask_secrets}` failed!"
32
32
  end
33
33
  end
34
34
 
@@ -136,6 +136,8 @@ module Gitlab
136
136
  end
137
137
 
138
138
  def login_params
139
+ return if Runtime::Env.skip_pull?
140
+
139
141
  if dev_gitlab_org?
140
142
  Runtime::Env.require_qa_dev_access_token!
141
143
 
@@ -145,14 +147,15 @@ module Gitlab
145
147
  registry: DEV_REGISTRY
146
148
  }
147
149
  elsif omnibus_mirror?
148
- username, password = if Runtime::Env.ci_job_token
150
+ username, password = if Runtime::Env.ci_job_token && Runtime::Env.ci_pipeline_source == 'pipeline'
149
151
  ['gitlab-ci-token', Runtime::Env.ci_job_token]
152
+ elsif Runtime::Env.qa_container_registry_access_token
153
+ [Runtime::Env.gitlab_username, Runtime::Env.qa_container_registry_access_token]
150
154
  else
151
155
  Runtime::Env.require_qa_access_token!
152
156
 
153
157
  [Runtime::Env.gitlab_username, Runtime::Env.qa_access_token]
154
158
  end
155
-
156
159
  {
157
160
  username: username,
158
161
  password: password,
@@ -173,6 +176,10 @@ module Gitlab
173
176
  canonical? || release.match?(CUSTOM_GITLAB_IMAGE_REGEX)
174
177
  end
175
178
 
179
+ def api_project_name
180
+ project_name.gsub('ce', 'foss').gsub('-ee', '')
181
+ end
182
+
176
183
  private
177
184
 
178
185
  def canonical?
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Gitlab
6
+ module QA
7
+ module Report
8
+ class JsonTestResults
9
+ include Enumerable
10
+
11
+ def initialize(file)
12
+ @testcases = JSON.parse(File.read(file))['examples'].map { |test| TestResult.from_json(test) }
13
+ end
14
+
15
+ def each(&block)
16
+ @testcases.each(&block)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module Gitlab
6
+ module QA
7
+ module Report
8
+ class JUnitTestResults
9
+ include Enumerable
10
+
11
+ def initialize(file)
12
+ @testcases = Nokogiri::XML(File.read(file)).xpath('//testcase').map { |test| TestResult.from_junit(test) }
13
+ end
14
+
15
+ def each(&block)
16
+ @testcases.each(&block)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -30,11 +30,12 @@ module Gitlab
30
30
  RETRY_BACK_OFF_DELAY = 60
31
31
  MAX_RETRY_ATTEMPTS = 3
32
32
 
33
- def initialize(token:, input_files:, project: nil)
33
+ def initialize(token:, input_files:, project: nil, input_format: :junit)
34
34
  @token = token
35
35
  @files = Array(input_files)
36
36
  @project = project
37
37
  @retry_backoff = 0
38
+ @input_format = input_format.to_sym
38
39
  end
39
40
 
40
41
  def invoke!
@@ -46,7 +47,14 @@ module Gitlab
46
47
 
47
48
  Dir.glob(files).each do |file|
48
49
  puts "Reporting tests in #{file}"
49
- Nokogiri::XML(File.open(file)).xpath('//testcase').each do |test|
50
+ case input_format
51
+ when :json
52
+ test_results = Report::JsonTestResults.new(file)
53
+ when :junit
54
+ test_results = Report::JUnitTestResults.new(file)
55
+ end
56
+
57
+ test_results.each do |test|
50
58
  report_test(test)
51
59
  end
52
60
  end
@@ -54,7 +62,7 @@ module Gitlab
54
62
 
55
63
  private
56
64
 
57
- attr_reader :files, :token, :project
65
+ attr_reader :files, :token, :project, :input_format
58
66
 
59
67
  def validate_input!
60
68
  assert_project!
@@ -99,9 +107,9 @@ module Gitlab
99
107
  end
100
108
 
101
109
  def report_test(test)
102
- return if test.search('skipped').any?
110
+ return if test.skipped
103
111
 
104
- puts "Reporting test: #{test['file']} | #{test['name']}"
112
+ puts "Reporting test: #{test.file} | #{test.name}"
105
113
 
106
114
  issue = find_issue(test)
107
115
  if issue
@@ -124,7 +132,7 @@ module Gitlab
124
132
  Gitlab.create_issue(
125
133
  project,
126
134
  title_from_test(test),
127
- { description: "### Full description\n\n#{search_safe(test['name'])}\n\n### File path\n\n#{test['file']}", labels: 'status::automated' }
135
+ { description: "### Full description\n\n#{search_safe(test.name)}\n\n### File path\n\n#{test.file}", labels: 'status::automated' }
128
136
  )
129
137
  end
130
138
  end
@@ -135,18 +143,18 @@ module Gitlab
135
143
  .auto_paginate
136
144
  .select { |issue| issue.state == 'opened' && issue.title.strip == title_from_test(test) }
137
145
 
138
- warn(%(Too many issues found with the file path "#{test['file']}" and name "#{test['name']}")) if issues.many?
146
+ warn(%(Too many issues found with the file path "#{test.file}" and name "#{test.name}")) if issues.many?
139
147
 
140
148
  issues.first
141
149
  end
142
150
  end
143
151
 
144
152
  def search_term(test)
145
- %("#{test['file']}" "#{search_safe(test['name'])}")
153
+ %("#{test.file}" "#{search_safe(test.name)}")
146
154
  end
147
155
 
148
156
  def title_from_test(test)
149
- title = "#{partial_file_path(test['file'])} | #{search_safe(test['name'])}".strip
157
+ title = "#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
150
158
 
151
159
  return title unless title.length > MAX_TITLE_LENGTH
152
160
 
@@ -162,7 +170,7 @@ module Gitlab
162
170
  end
163
171
 
164
172
  def note_status(issue, test)
165
- return if failures(test).empty?
173
+ return if test.failures.empty?
166
174
 
167
175
  note = note_content(test)
168
176
 
@@ -176,7 +184,7 @@ module Gitlab
176
184
  end
177
185
 
178
186
  def note_content(test)
179
- errors = failures(test).each_with_object([]) do |failure, text|
187
+ errors = test.failures.each_with_object([]) do |failure, text|
180
188
  text << <<~TEXT
181
189
  Error:
182
190
  ```
@@ -185,7 +193,7 @@ module Gitlab
185
193
 
186
194
  Stacktrace:
187
195
  ```
188
- #{failure.content}
196
+ #{failure['stacktrace']}
189
197
  ```
190
198
  TEXT
191
199
  end.join("\n\n")
@@ -231,7 +239,7 @@ module Gitlab
231
239
  def update_labels(issue, test)
232
240
  labels = issue.labels
233
241
  labels.delete_if { |label| label.start_with?("#{pipeline}::") }
234
- labels << (failures(test).empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
242
+ labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
235
243
  labels << "Enterprise Edition" if ee_test?(test)
236
244
  quarantine_job? ? labels << "quarantine" : labels.delete("quarantine")
237
245
 
@@ -242,11 +250,7 @@ module Gitlab
242
250
  # rubocop:enable Metrics/AbcSize
243
251
 
244
252
  def ee_test?(test)
245
- test['file'] =~ %r{features/ee/(api|browser_ui)}
246
- end
247
-
248
- def failures(test)
249
- test.search('failure')
253
+ test.file =~ %r{features/ee/(api|browser_ui)}
250
254
  end
251
255
 
252
256
  def pipeline
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module QA
5
+ module Report
6
+ class TestResult
7
+ attr_accessor :name, :file, :skipped, :failures
8
+
9
+ def self.from_json(test)
10
+ new.tap do |test_result|
11
+ test_result.name = test['full_description']
12
+ test_result.file = test['file_path']
13
+ test_result.skipped = test['status'] == 'pending'
14
+ test_result.failures = failures_from_json_exceptions(test)
15
+ end
16
+ end
17
+
18
+ def self.from_junit(test)
19
+ new.tap do |test_result|
20
+ test_result.name = test['name']
21
+ test_result.file = test['file']
22
+ test_result.skipped = test.search('skipped').any?
23
+ test_result.failures = failures_from_junit_exceptions(test)
24
+ end
25
+ end
26
+
27
+ def self.failures_from_json_exceptions(test)
28
+ return [] unless test.key?('exceptions')
29
+
30
+ test['exceptions'].map do |exception|
31
+ {
32
+ 'message' => "#{exception['class']}: #{exception['message']}",
33
+ 'stacktrace' => exception['backtrace'].join('\n')
34
+ }
35
+ end
36
+ end
37
+ private_class_method :failures_from_json_exceptions
38
+
39
+ def self.failures_from_junit_exceptions(test)
40
+ failures = test.search('failure')
41
+ return [] if failures.empty?
42
+
43
+ failures.map do |exception|
44
+ {
45
+ 'message' => "#{exception['type']}: #{exception['message']}",
46
+ 'stacktrace' => exception.content
47
+ }
48
+ end
49
+ end
50
+ private_class_method :failures_from_junit_exceptions
51
+ end
52
+ end
53
+ end
54
+ end
@@ -4,22 +4,20 @@ module Gitlab
4
4
  module QA
5
5
  # rubocop:disable Metrics/AbcSize
6
6
  class Runner
7
- # These options are implemented in the QA framework (i.e., in the CE/EE codebase)
8
- # They're included here so that gitlab-qa treats them as valid options
9
- PASS_THROUGH_OPTS = [
10
- ['--address URL', 'Address of the instance to test'],
11
- ['--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'],
12
- ['--mattermost-address URL', 'Address of the Mattermost server'],
13
- ['--parallel', 'Execute tests in parallel'],
14
- ['--loop', 'Execute tests in a loop']
15
- ].freeze
16
-
17
7
  def self.run(args)
18
- options = OptionParser.new do |opts|
8
+ Runtime::Scenario.define(:teardown, true)
9
+ Runtime::Scenario.define(:run_tests, true)
10
+
11
+ @options = OptionParser.new do |opts|
19
12
  opts.banner = 'Usage: gitlab-qa [options] Scenario URL [[--] path] [rspec_options]'
20
13
 
21
- PASS_THROUGH_OPTS.each do |opt|
22
- opts.on(*opt)
14
+ opts.on('--no-teardown', 'Skip teardown of containers after the scenario completes.') do
15
+ Runtime::Scenario.define(:teardown, false)
16
+ end
17
+
18
+ opts.on('--no-tests', 'Orchestrates the docker containers but does not run the tests. Implies --no-teardown') do
19
+ Runtime::Scenario.define(:run_tests, false)
20
+ Runtime::Scenario.define(:teardown, false)
23
21
  end
24
22
 
25
23
  opts.on_tail('-v', '--version', 'Show the version') do
@@ -33,19 +31,29 @@ module Gitlab
33
31
  exit
34
32
  end
35
33
 
36
- opts.parse(args)
34
+ begin
35
+ opts.parse(args)
36
+ rescue OptionParser::InvalidOption
37
+ # Ignore invalid options and options that are passed through to the tests
38
+ end
37
39
  end
38
40
 
41
+ args.reject! { |arg| gitlab_qa_options.include?(arg) }
42
+
39
43
  if args.size >= 1
40
44
  Scenario
41
45
  .const_get(args.shift)
42
46
  .perform(*args)
43
47
  else
44
- puts options
48
+ puts @options
45
49
  exit 1
46
50
  end
47
51
  end
48
52
  # rubocop:enable Metrics/AbcSize
53
+
54
+ def self.gitlab_qa_options
55
+ @gitlab_qa_options ||= @options.top.list.map(&:long).flatten
56
+ end
49
57
  end
50
58
  end
51
59
  end
@@ -6,6 +6,8 @@ module Gitlab
6
6
  module Env
7
7
  extend self
8
8
 
9
+ # Variables that are used in tests and are passed through to the docker container that executes the tests.
10
+ # These variables should be listed in /docs/what_tests_can_be_run.md#supported-gitlab-environment-variables
9
11
  ENV_VARIABLES = {
10
12
  'QA_REMOTE_GRID' => :remote_grid,
11
13
  'QA_REMOTE_GRID_USERNAME' => :remote_grid_username,
@@ -38,6 +40,7 @@ module Gitlab
38
40
  'SIGNUP_DISABLED' => :signup_disabled,
39
41
  'QA_ADDITIONAL_REPOSITORY_STORAGE' => :qa_additional_repository_storage,
40
42
  'QA_PRAEFECT_REPOSITORY_STORAGE' => :qa_praefect_repository_storage,
43
+ 'QA_GITALY_NON_CLUSTER_STORAGE' => :qa_gitaly_non_cluster_storage,
41
44
  'QA_COOKIES' => :qa_cookie,
42
45
  'QA_DEBUG' => :qa_debug,
43
46
  'QA_LOG_PATH' => :qa_log_path,
@@ -112,6 +115,10 @@ module Gitlab
112
115
  ENV['CI_JOB_URL']
113
116
  end
114
117
 
118
+ def ci_pipeline_source
119
+ ENV['CI_PIPELINE_SOURCE']
120
+ end
121
+
115
122
  def ci_project_name
116
123
  ENV['CI_PROJECT_NAME']
117
124
  end
@@ -148,6 +155,10 @@ module Gitlab
148
155
  ENV['GITLAB_QA_DEV_ACCESS_TOKEN']
149
156
  end
150
157
 
158
+ def qa_container_registry_access_token
159
+ ENV['GITLAB_QA_CONTAINER_REGISTRY_ACCESS_TOKEN']
160
+ end
161
+
151
162
  def host_artifacts_dir
152
163
  @host_artifacts_dir ||= File.join(ENV['QA_ARTIFACTS_DIR'] || '/tmp/gitlab-qa', Runtime::Env.run_id)
153
164
  end
@@ -8,9 +8,6 @@ module Gitlab
8
8
 
9
9
  class Geo < Scenario::Template
10
10
  def perform(release, primary_address, secondary_address, *rspec_args)
11
- # Geo requires an EE license
12
- Runtime::Env.require_license!
13
-
14
11
  Component::Specs.perform do |specs|
15
12
  specs.suite = 'QA::EE::Scenario::Test::Geo'
16
13
  specs.release = QA::Release.new(release)