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.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +41 -2
- data/README.md +52 -5
- data/docs/what_tests_can_be_run.md +101 -5
- data/lib/gitlab/qa.rb +4 -1
- data/lib/gitlab/qa/component/base.rb +32 -11
- data/lib/gitlab/qa/component/specs.rb +43 -11
- data/lib/gitlab/qa/docker/command.rb +15 -3
- data/lib/gitlab/qa/docker/engine.rb +5 -1
- data/lib/gitlab/qa/docker/shellout.rb +2 -2
- data/lib/gitlab/qa/release.rb +9 -2
- data/lib/gitlab/qa/report/json_test_results.rb +21 -0
- data/lib/gitlab/qa/report/junit_test_results.rb +21 -0
- data/lib/gitlab/qa/report/results_in_issues.rb +22 -18
- data/lib/gitlab/qa/report/test_result.rb +54 -0
- data/lib/gitlab/qa/runner.rb +23 -15
- data/lib/gitlab/qa/runtime/env.rb +11 -0
- data/lib/gitlab/qa/scenario/test/instance/geo.rb +0 -3
- data/lib/gitlab/qa/scenario/test/integration/geo.rb +4 -2
- data/lib/gitlab/qa/scenario/test/integration/gitaly_cluster.rb +204 -0
- data/lib/gitlab/qa/scenario/test/integration/praefect.rb +20 -8
- data/lib/gitlab/qa/scenario/test/sanity/version.rb +1 -1
- data/lib/gitlab/qa/version.rb +1 -1
- metadata +6 -3
- data/lib/gitlab/qa/scenario/test/integration/gitaly_ha.rb +0 -166
@@ -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
|
|
data/lib/gitlab/qa/release.rb
CHANGED
@@ -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
|
-
|
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.
|
110
|
+
return if test.skipped
|
103
111
|
|
104
|
-
puts "Reporting test: #{test
|
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
|
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
|
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
|
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
|
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
|
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 =
|
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
|
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 << (
|
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
|
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
|
data/lib/gitlab/qa/runner.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
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)
|