gitlab-qa 5.14.1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)