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.
- 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)
|