Commit 4a1d3f48 authored by Mark Lapierre's avatar Mark Lapierre

Add e2e tests for MR push options

- Adds 5 spec files for different push options
- `Git::Repository` and `Repository::Push` can now handle push options
- Updates `Resource::MergeRequest` to merge via the API
- `Resource::Project` can now return merge requests
- Allow waiting for merges to appear in the activity feed (events API)
parent f8384ca0
...@@ -59,6 +59,10 @@ module QA ...@@ -59,6 +59,10 @@ module QA
self.username, self.password = default_credentials self.username, self.password = default_credentials
end end
def use_default_identity
configure_identity('GitLab QA', 'root@gitlab.com')
end
def clone(opts = '') def clone(opts = '')
clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3) clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3)
return clone_result.response unless clone_result.success? return clone_result.response unless clone_result.success?
...@@ -122,8 +126,12 @@ module QA ...@@ -122,8 +126,12 @@ module QA
run("git rev-parse --abbrev-ref HEAD").to_s run("git rev-parse --abbrev-ref HEAD").to_s
end end
def push_changes(branch = 'master') def push_changes(branch = 'master', push_options: nil)
run("git push #{uri} #{branch}", max_attempts: 3).to_s cmd = ['git push']
cmd << push_options_hash_to_string(push_options)
cmd << uri
cmd << branch
run(cmd.compact.join(' '), max_attempts: 3).to_s
end end
def push_all_branches def push_all_branches
...@@ -293,6 +301,23 @@ module QA ...@@ -293,6 +301,23 @@ module QA
@tmp_home_dir ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) @tmp_home_dir ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s)
end end
def push_options_hash_to_string(opts)
return if opts.nil?
prefix = "-o merge_request"
opts.each_with_object([]) do |(key, value), options|
if value.is_a?(Array)
value.each do |item|
options << "#{prefix}.#{key}=\"#{item}\""
end
elsif value == true
options << "#{prefix}.#{key}"
else
options << "#{prefix}.#{key}=\"#{value}\""
end
end.join(' ')
end
def netrc_file_path def netrc_file_path
@netrc_file_path ||= File.join(tmp_home_dir, '.netrc') @netrc_file_path ||= File.join(tmp_home_dir, '.netrc')
end end
......
...@@ -9,9 +9,12 @@ module QA ...@@ -9,9 +9,12 @@ module QA
EventNotFoundError = Class.new(RuntimeError) EventNotFoundError = Class.new(RuntimeError)
module Base module Base
def events(action: nil) def events(action: nil, target_type: nil)
query = []
query << "action=#{CGI.escape(action)}" if action
query << "target_type=#{CGI.escape(target_type)}" if target_type
path = [api_get_events] path = [api_get_events]
path << "?action=#{CGI.escape(action)}" if action path << "?#{query.join("&")}" unless query.empty?
parse_body(api_get_from("#{path.join}")) parse_body(api_get_from("#{path.join}"))
end end
......
...@@ -6,6 +6,13 @@ module QA ...@@ -6,6 +6,13 @@ module QA
module Project module Project
include Events::Base include Events::Base
def wait_for_merge(title)
QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_merge with title "#{title}"])
wait_for_event do
events(action: 'accepted', target_type: 'merge_request').any? { |event| event[:target_title] == title }
end
end
def wait_for_push(commit_message) def wait_for_push(commit_message)
QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_push with commit message "#{commit_message}"]) QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_push with commit message "#{commit_message}"])
wait_for_event do wait_for_event do
......
# frozen_string_literal: true # frozen_string_literal: true
require 'securerandom' require 'securerandom'
require 'active_support/core_ext/object/blank'
module QA module QA
module Resource module Resource
...@@ -17,7 +18,12 @@ module QA ...@@ -17,7 +18,12 @@ module QA
:labels, :labels,
:file_name, :file_name,
:file_content :file_content
attr_writer :no_preparation attr_writer :no_preparation,
:wait_for_merge
attribute :merge_when_pipeline_succeeds
attribute :merge_status
attribute :state
attribute :project do attribute :project do
Project.fabricate! do |resource| Project.fabricate! do |resource|
...@@ -58,6 +64,7 @@ module QA ...@@ -58,6 +64,7 @@ module QA
@file_content = "File Added" @file_content = "File Added"
@target_new_branch = true @target_new_branch = true
@no_preparation = false @no_preparation = false
@wait_for_merge = true
end end
def fabricate! def fabricate!
...@@ -80,10 +87,17 @@ module QA ...@@ -80,10 +87,17 @@ module QA
end end
def fabricate_via_api! def fabricate_via_api!
resource_web_url(api_get)
rescue ResourceNotFoundError
populate(:target, :source) unless @no_preparation populate(:target, :source) unless @no_preparation
super super
end end
def api_merge_path
"/projects/#{project.id}/merge_requests/#{id}/merge"
end
def api_get_path def api_get_path
"/projects/#{project.id}/merge_requests/#{id}" "/projects/#{project.id}/merge_requests/#{id}"
end end
...@@ -100,6 +114,36 @@ module QA ...@@ -100,6 +114,36 @@ module QA
title: @title title: @title
} }
end end
def merge_via_api!
Support::Waiter.wait_until(sleep_interval: 1) do
QA::Runtime::Logger.debug("Waiting until merge request with id '#{id}' can be merged")
reload!.api_resource[:merge_status] == 'can_be_merged'
end
Support::Retrier.retry_on_exception do
response = put(Runtime::API::Request.new(api_client, api_merge_path).url)
unless response.code == HTTP_STATUS_OK
raise ResourceUpdateFailedError, "Could not merge. Request returned (#{response.code}): `#{response}`."
end
result = parse_body(response)
project.wait_for_merge(result[:title]) if @wait_for_merge
result
end
end
private
def transform_api_resource(api_resource)
raise ResourceNotFoundError if api_resource.blank?
super(api_resource)
end
end end
end end
end end
...@@ -110,6 +110,10 @@ module QA ...@@ -110,6 +110,10 @@ module QA
response.any? { |file| file[:path] == file_path } response.any? { |file| file[:path] == file_path }
end end
def has_branch?(branch)
has_branches?(Array(branch))
end
def has_branches?(branches) def has_branches?(branches)
branches.all? do |branch| branches.all? do |branch|
response = get(Runtime::API::Request.new(api_client, "#{api_repository_branches_path}/#{branch}").url) response = get(Runtime::API::Request.new(api_client, "#{api_repository_branches_path}/#{branch}").url)
...@@ -140,6 +144,10 @@ module QA ...@@ -140,6 +144,10 @@ module QA
"#{api_get_path}/members" "#{api_get_path}/members"
end end
def api_merge_requests_path
"#{api_get_path}/merge_requests"
end
def api_runners_path def api_runners_path
"#{api_get_path}/runners" "#{api_get_path}/runners"
end end
...@@ -223,6 +231,14 @@ module QA ...@@ -223,6 +231,14 @@ module QA
result[:import_status] result[:import_status]
end end
def merge_requests
parse_body(get(Runtime::API::Request.new(api_client, api_merge_requests_path).url))
end
def merge_request_with_title(title)
merge_requests.find { |mr| mr[:title] == title }
end
def runners(tag_list: nil) def runners(tag_list: nil)
response = if tag_list response = if tag_list
get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
......
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
:branch_name, :new_branch, :output, :repository_http_uri, :branch_name, :new_branch, :output, :repository_http_uri,
:repository_ssh_uri, :ssh_key, :user, :use_lfs, :tag_name :repository_ssh_uri, :ssh_key, :user, :use_lfs, :tag_name
attr_writer :remote_branch, :gpg_key_id attr_writer :remote_branch, :gpg_key_id, :merge_request_push_options
def initialize def initialize
@file_name = "file-#{SecureRandom.hex(8)}.txt" @file_name = "file-#{SecureRandom.hex(8)}.txt"
...@@ -24,6 +24,7 @@ module QA ...@@ -24,6 +24,7 @@ module QA
@use_lfs = false @use_lfs = false
@tag_name = nil @tag_name = nil
@gpg_key_id = nil @gpg_key_id = nil
@merge_request_push_options = nil
end end
def remote_branch def remote_branch
...@@ -95,7 +96,7 @@ module QA ...@@ -95,7 +96,7 @@ module QA
end end
@output += commit_to repository @output += commit_to repository
@output += repository.push_changes("#{branch_name}:#{remote_branch}") @output += repository.push_changes("#{branch_name}:#{remote_branch}", push_options: @merge_request_push_options)
end end
repository.delete_ssh_key repository.delete_ssh_key
......
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Merge request push options' do
# If run locally on GDK, push options need to be enabled on the host with the following command:
#
# git config --global receive.advertisepushoptions true
branch = "push-options-test-#{SecureRandom.hex(8)}"
title = "MR push options test #{SecureRandom.hex(8)}"
commit_message = 'Add README.md'
project = Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-push-options'
project.initialize_with_readme = true
end
it 'sets labels' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.commit_message = commit_message
push.branch_name = branch
push.merge_request_push_options = {
create: true,
title: title,
label: %w[one two three]
}
end
merge_request = project.merge_request_with_title(title)
expect(merge_request[:labels]).to include('one').and include('two').and include('three')
end
context 'when labels are set already' do
it 'removes them' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_content = "Unlabel test #{SecureRandom.hex(8)}"
push.commit_message = commit_message
push.branch_name = branch
push.new_branch = false
push.merge_request_push_options = {
title: title,
unlabel: %w[one three]
}
end
merge_request = project.merge_request_with_title(title)
aggregate_failures do
expect(merge_request[:labels]).to include('two')
expect(merge_request[:labels]).not_to include('one')
expect(merge_request[:labels]).not_to include('three')
end
end
end
end
end
end
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Merge request push options' do
# If run locally on GDK, push options need to be enabled on the host with the following command:
#
# git config --global receive.advertisepushoptions true
let(:branch) { "push-options-test-#{SecureRandom.hex(8)}" }
let(:title) { "MR push options test #{SecureRandom.hex(8)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-push-options'
project.initialize_with_readme = true
end
end
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = "runner-for-#{project.name}"
runner.tags = ["runner-for-#{project.name}"]
end
end
after do
runner.remove_via_api!
end
it 'sets merge when pipeline succeeds' do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
no-op:
tags:
- "runner-for-#{project.name}"
script: sleep 999 # Leave the pipeline pending
YAML
}
]
)
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = branch
push.merge_request_push_options = {
create: true,
merge_when_pipeline_succeeds: true,
title: title
}
end
merge_request = project.merge_request_with_title(title)
merge_request = Resource::MergeRequest.fabricate_via_api! do |mr|
mr.project = project
mr.id = merge_request[:iid]
end
expect(merge_request.state).to eq('opened')
expect(merge_request.merge_status).to eq('checking')
expect(merge_request.merge_when_pipeline_succeeds).to be true
end
it 'merges when pipeline succeeds' do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
no-op:
tags:
- "runner-for-#{project.name}"
script: echo 'OK'
YAML
}
]
)
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = branch
push.merge_request_push_options = {
create: true,
merge_when_pipeline_succeeds: true,
title: title
}
end
merge_request = project.merge_request_with_title(title)
expect(merge_request[:merge_when_pipeline_succeeds]).to be true
merge_request = Support::Waiter.wait_until(sleep_interval: 5) do
mr = Resource::MergeRequest.fabricate_via_api! do |mr|
mr.project = project
mr.id = merge_request[:iid]
end
next unless mr.state == 'merged'
mr
end
expect(merge_request.state).to eq('merged')
end
end
end
end
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Merge request push options' do
# If run locally on GDK, push options need to be enabled on the host with the following command:
#
# git config --global receive.advertisepushoptions true
let(:branch) { "push-options-test-#{SecureRandom.hex(8)}" }
let(:title) { "MR push options test #{SecureRandom.hex(8)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-push-options'
project.initialize_with_readme = true
end
end
it 'removes the source branch' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = branch
push.merge_request_push_options = {
create: true,
remove_source_branch: true,
title: title
}
end
merge_request = project.merge_request_with_title(title)
merge_request = Resource::MergeRequest.fabricate_via_api! do |mr|
mr.project = project
mr.id = merge_request[:iid]
end.merge_via_api!
expect(merge_request[:state]).to eq('merged')
expect(project).not_to have_branch(branch)
end
end
end
end
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Merge request push options' do
# If run locally on GDK, push options need to be enabled on the host with the following command:
#
# git config --global receive.advertisepushoptions true
let(:title) { "MR push options test #{SecureRandom.hex(8)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-push-options'
project.initialize_with_readme = true
end
end
it 'sets a target branch' do
target_branch = "push-options-test-target-#{SecureRandom.hex(8)}"
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = target_branch
push.file_content = "Target branch test target branch #{SecureRandom.hex(8)}"
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = "push-options-test-#{SecureRandom.hex(8)}"
push.file_content = "Target branch test source branch #{SecureRandom.hex(8)}"
push.merge_request_push_options = {
create: true,
title: title,
target: target_branch
}
end
merge_request = project.merge_request_with_title(title)
expect(merge_request[:target_branch]).to eq(target_branch)
merge_request = Resource::MergeRequest.fabricate_via_api! do |mr|
mr.project = project
mr.id = merge_request[:iid]
end.merge_via_api!
expect(merge_request[:state]).to eq('merged')
end
end
end
end
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'Merge request push options' do
# If run locally on GDK, push options need to be enabled on the host with the following command:
#
# git config --global receive.advertisepushoptions true
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-push-options'
project.initialize_with_readme = true
end
end
it 'sets title and description' do
description = "This is a test of MR push options"
title = "MR push options test #{SecureRandom.hex(8)}"
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = "push-options-test-#{SecureRandom.hex(8)}"
push.merge_request_push_options = {
create: true,
title: title,
description: description
}
end
merge_request = project.merge_request_with_title(title)
aggregate_failures do
expect(merge_request[:title]).to eq(title)
expect(merge_request[:description]).to eq(description)
end
merge_request = Resource::MergeRequest.fabricate_via_api! do |mr|
mr.project = project
mr.id = merge_request[:iid]
end.merge_via_api!
expect(merge_request[:state]).to eq('merged')
end
end
end
end
...@@ -40,7 +40,7 @@ module QA ...@@ -40,7 +40,7 @@ module QA
return_response_or_raise(e) return_response_or_raise(e)
end end
def put(url, payload) def put(url, payload = nil)
RestClient::Request.execute( RestClient::Request.execute(
method: :put, method: :put,
url: url, url: url,
......
...@@ -117,6 +117,66 @@ describe QA::Git::Repository do ...@@ -117,6 +117,66 @@ describe QA::Git::Repository do
let(:call_method) { repository.push_changes(branch) } let(:call_method) { repository.push_changes(branch) }
end end
end end
context 'with push options' do
let(:command) { "git push #{push_options} #{repo_uri_with_credentials} #{branch}" }
context 'when set to create a merge request' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.create' }
let(:call_method) { repository.push_changes(push_options: { create: true }) }
end
end
context 'when set to merge when pipeline succeeds' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.merge_when_pipeline_succeeds' }
let(:call_method) { repository.push_changes(push_options: { merge_when_pipeline_succeeds: true }) }
end
end
context 'when set to remove source branch' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.remove_source_branch' }
let(:call_method) { repository.push_changes(push_options: { remove_source_branch: true }) }
end
end
context 'when title is given' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.title="Is A Title"' }
let(:call_method) { repository.push_changes(push_options: { title: 'Is A Title' }) }
end
end
context 'when description is given' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.description="Is A Description"' }
let(:call_method) { repository.push_changes(push_options: { description: 'Is A Description' }) }
end
end
context 'when target branch is given' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.target="is-a-target-branch"' }
let(:call_method) { repository.push_changes(push_options: { target: 'is-a-target-branch' }) }
end
end
context 'when a label is given' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.label="is-a-label"' }
let(:call_method) { repository.push_changes(push_options: { label: ['is-a-label'] }) }
end
end
context 'when two labels are given' do
it_behaves_like 'command with retries' do
let(:push_options) { '-o merge_request.label="is-a-label" -o merge_request.label="is-another-label"' }
let(:call_method) { repository.push_changes(push_options: { label: %w[is-a-label is-another-label] }) }
end
end
end
end end
describe '#git_protocol=' do describe '#git_protocol=' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment