2016-12-20 14:53:53 +00:00
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
describe Projects::PipelinesController do
|
2017-05-06 16:45:46 +00:00
|
|
|
include ApiHelpers
|
|
|
|
|
2017-10-12 08:39:11 +00:00
|
|
|
set(:user) { create(:user) }
|
|
|
|
set(:project) { create(:project, :public, :repository) }
|
2017-06-07 10:32:16 +00:00
|
|
|
let(:feature) { ProjectFeature::DISABLED }
|
2016-12-20 14:53:53 +00:00
|
|
|
|
|
|
|
before do
|
2017-07-18 14:32:34 +00:00
|
|
|
stub_not_protect_default_branch
|
|
|
|
project.add_developer(user)
|
2017-10-12 08:39:11 +00:00
|
|
|
project.project_feature.update(builds_access_level: feature)
|
2017-05-04 19:01:15 +00:00
|
|
|
|
2016-12-20 14:53:53 +00:00
|
|
|
sign_in(user)
|
|
|
|
end
|
|
|
|
|
2016-12-21 14:13:24 +00:00
|
|
|
describe 'GET index.json' do
|
|
|
|
before do
|
2017-12-05 13:15:30 +00:00
|
|
|
%w(pending running created success).each_with_index do |status, index|
|
|
|
|
sha = project.commit("HEAD~#{index}")
|
|
|
|
create(:ci_empty_pipeline, status: status, project: project, sha: sha)
|
|
|
|
end
|
2017-10-12 08:39:11 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
subject do
|
|
|
|
get :index, namespace_id: project.namespace, project_id: project, format: :json
|
2016-12-21 14:13:24 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns JSON with serialized pipelines' do
|
2017-10-12 08:39:11 +00:00
|
|
|
subject
|
|
|
|
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2017-05-06 16:45:46 +00:00
|
|
|
expect(response).to match_response_schema('pipeline')
|
2016-12-21 14:13:24 +00:00
|
|
|
|
|
|
|
expect(json_response).to include('pipelines')
|
2017-02-23 13:44:58 +00:00
|
|
|
expect(json_response['pipelines'].count).to eq 4
|
Limit the number of pipelines to count
When displaying the project pipelines dashboard we display a few tabs
for different pipeline states. For every such tab we count the number of
pipelines that belong to it. For large projects such as GitLab CE this
means having to count over 80 000 rows, which can easily take between 70
and 100 milliseconds per query.
To improve this we apply a technique we already use for search results:
we limit the number of rows to count. The current limit is 1000, which
means that if more than 1000 rows are present for a state we will show
"1000+" instead of the exact number. The SQL queries used for this
perform much better than a regular COUNT, even when a project has a lot
of pipelines.
Prior to these changes we would end up running a query like this:
SELECT COUNT(*)
FROM ci_pipelines
WHERE project_id = 13083
AND status IN ('success', 'failed', 'canceled')
This would produce a plan along the lines of the following:
Aggregate (cost=3147.55..3147.56 rows=1 width=8) (actual time=501.413..501.413 rows=1 loops=1)
Buffers: shared hit=17116 read=861 dirtied=2
-> Index Only Scan using index_ci_pipelines_on_project_id_and_ref_and_status_and_id on ci_pipelines (cost=0.56..2984.14 rows=65364 width=0) (actual time=0.095..490.263 rows=80388 loops=1)
Index Cond: (project_id = 13083)
Filter: ((status)::text = ANY ('{success,failed,canceled}'::text[]))
Rows Removed by Filter: 2894
Heap Fetches: 353
Buffers: shared hit=17116 read=861 dirtied=2
Planning time: 1.409 ms
Execution time: 501.519 ms
Using the LIMIT count technique we instead run the following query:
SELECT COUNT(*)
FROM (
SELECT 1
FROM ci_pipelines
WHERE project_id = 13083
AND status IN ('success', 'failed', 'canceled')
LIMIT 1001
) for_count
This query produces the following plan:
Aggregate (cost=58.77..58.78 rows=1 width=8) (actual time=1.726..1.727 rows=1 loops=1)
Buffers: shared hit=169 read=15
-> Limit (cost=0.56..46.25 rows=1001 width=4) (actual time=0.164..1.570 rows=1001 loops=1)
Buffers: shared hit=169 read=15
-> Index Only Scan using index_ci_pipelines_on_project_id_and_ref_and_status_and_id on ci_pipelines (cost=0.56..2984.14 rows=65364 width=4) (actual time=0.162..1.426 rows=1001 loops=1)
Index Cond: (project_id = 13083)
Filter: ((status)::text = ANY ('{success,failed,canceled}'::text[]))
Rows Removed by Filter: 9
Heap Fetches: 10
Buffers: shared hit=169 read=15
Planning time: 1.832 ms
Execution time: 1.821 ms
While this query still uses a Filter for the "status" field the number
of rows that it may end up filtering (at most 1001) is small enough that
an additional index does not appear to be necessary at this time.
See https://gitlab.com/gitlab-org/gitlab-ce/issues/43132#note_68659234
for more information.
2018-04-17 13:13:38 +00:00
|
|
|
expect(json_response['count']['all']).to eq '4'
|
|
|
|
expect(json_response['count']['running']).to eq '1'
|
|
|
|
expect(json_response['count']['pending']).to eq '1'
|
|
|
|
expect(json_response['count']['finished']).to eq '1'
|
2016-12-21 14:13:24 +00:00
|
|
|
end
|
2017-10-12 08:39:11 +00:00
|
|
|
|
|
|
|
context 'when performing gitaly calls', :request_store do
|
|
|
|
it 'limits the Gitaly requests' do
|
2017-12-05 13:15:30 +00:00
|
|
|
expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(3)
|
2017-10-12 08:39:11 +00:00
|
|
|
end
|
|
|
|
end
|
2016-12-21 14:13:24 +00:00
|
|
|
end
|
|
|
|
|
2017-05-06 16:45:46 +00:00
|
|
|
describe 'GET show JSON' do
|
2017-05-10 16:39:17 +00:00
|
|
|
let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
|
2017-05-06 16:45:46 +00:00
|
|
|
|
|
|
|
it 'returns the pipeline' do
|
|
|
|
get_pipeline_json
|
|
|
|
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2017-05-06 16:45:46 +00:00
|
|
|
expect(json_response).not_to be_an(Array)
|
|
|
|
expect(json_response['id']).to be(pipeline.id)
|
|
|
|
expect(json_response['details']).to have_key 'stages'
|
|
|
|
end
|
|
|
|
|
2017-06-09 17:12:51 +00:00
|
|
|
context 'when the pipeline has multiple stages and groups', :request_store do
|
2017-05-10 16:39:17 +00:00
|
|
|
before do
|
|
|
|
create_build('build', 0, 'build')
|
|
|
|
create_build('test', 1, 'rspec 0')
|
|
|
|
create_build('deploy', 2, 'production')
|
|
|
|
create_build('post deploy', 3, 'pages 0')
|
|
|
|
end
|
|
|
|
|
2017-08-01 18:51:52 +00:00
|
|
|
let(:project) { create(:project, :repository) }
|
2017-05-10 16:39:17 +00:00
|
|
|
let(:pipeline) do
|
|
|
|
create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id)
|
|
|
|
end
|
|
|
|
|
2017-05-06 16:45:46 +00:00
|
|
|
it 'does not perform N + 1 queries' do
|
|
|
|
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
|
|
|
|
|
2017-05-10 16:39:17 +00:00
|
|
|
create_build('test', 1, 'rspec 1')
|
|
|
|
create_build('test', 1, 'spinach 0')
|
|
|
|
create_build('test', 1, 'spinach 1')
|
|
|
|
create_build('test', 1, 'audit')
|
|
|
|
create_build('post deploy', 3, 'pages 1')
|
|
|
|
create_build('post deploy', 3, 'pages 2')
|
2017-05-06 16:45:46 +00:00
|
|
|
|
2017-05-10 16:39:17 +00:00
|
|
|
new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
|
|
|
|
expect(new_count).to be_within(12).of(control_count)
|
2017-05-06 16:45:46 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_pipeline_json
|
|
|
|
get :show, namespace_id: project.namespace, project_id: project, id: pipeline, format: :json
|
|
|
|
end
|
2017-05-10 16:39:17 +00:00
|
|
|
|
|
|
|
def create_build(stage, stage_idx, name)
|
|
|
|
create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
|
|
|
|
end
|
2017-05-06 16:45:46 +00:00
|
|
|
end
|
|
|
|
|
2016-12-20 14:53:53 +00:00
|
|
|
describe 'GET stages.json' do
|
2016-12-21 14:13:24 +00:00
|
|
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
|
2016-12-20 14:53:53 +00:00
|
|
|
context 'when accessing existing stage' do
|
|
|
|
before do
|
|
|
|
create(:ci_build, pipeline: pipeline, stage: 'build')
|
|
|
|
|
|
|
|
get_stage('build')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns html source for stage dropdown' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2018-05-01 21:56:33 +00:00
|
|
|
expect(response).to match_response_schema('pipeline_stage')
|
2016-12-20 14:53:53 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when accessing unknown stage' do
|
|
|
|
before do
|
|
|
|
get_stage('test')
|
|
|
|
end
|
|
|
|
|
2016-12-20 19:07:34 +00:00
|
|
|
it 'responds with not found' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
2016-12-20 19:07:34 +00:00
|
|
|
end
|
2016-12-20 14:53:53 +00:00
|
|
|
end
|
|
|
|
|
2016-12-20 19:07:34 +00:00
|
|
|
def get_stage(name)
|
2017-02-23 23:55:01 +00:00
|
|
|
get :stage, namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
2016-12-20 19:07:34 +00:00
|
|
|
id: pipeline.id,
|
|
|
|
stage: name,
|
|
|
|
format: :json
|
|
|
|
end
|
2016-12-20 14:53:53 +00:00
|
|
|
end
|
2017-03-06 12:01:18 +00:00
|
|
|
|
2018-05-01 21:56:33 +00:00
|
|
|
describe 'GET stages_ajax.json' do
|
|
|
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
|
|
|
|
context 'when accessing existing stage' do
|
|
|
|
before do
|
|
|
|
create(:ci_build, pipeline: pipeline, stage: 'build')
|
|
|
|
|
|
|
|
get_stage_ajax('build')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns html source for stage dropdown' do
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
|
|
expect(response).to render_template('projects/pipelines/_stage')
|
|
|
|
expect(json_response).to include('html')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when accessing unknown stage' do
|
|
|
|
before do
|
|
|
|
get_stage_ajax('test')
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'responds with not found' do
|
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_stage_ajax(name)
|
|
|
|
get :stage_ajax, namespace_id: project.namespace,
|
2018-05-02 16:00:12 +00:00
|
|
|
project_id: project,
|
|
|
|
id: pipeline.id,
|
|
|
|
stage: name,
|
|
|
|
format: :json
|
2018-05-01 21:56:33 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-03-06 12:01:18 +00:00
|
|
|
describe 'GET status.json' do
|
2017-03-21 13:21:13 +00:00
|
|
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
let(:status) { pipeline.detailed_status(double('user')) }
|
2017-03-10 17:44:41 +00:00
|
|
|
|
2017-03-21 13:21:13 +00:00
|
|
|
before do
|
|
|
|
get :status, namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
|
|
|
id: pipeline.id,
|
|
|
|
format: :json
|
|
|
|
end
|
2017-03-06 12:01:18 +00:00
|
|
|
|
2017-03-21 13:21:13 +00:00
|
|
|
it 'return a detailed pipeline status in json' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
2017-03-21 13:21:13 +00:00
|
|
|
expect(json_response['text']).to eq status.text
|
|
|
|
expect(json_response['label']).to eq status.label
|
|
|
|
expect(json_response['icon']).to eq status.icon
|
2017-10-03 14:47:56 +00:00
|
|
|
expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
|
2017-03-06 12:01:18 +00:00
|
|
|
end
|
|
|
|
end
|
2017-05-04 19:01:15 +00:00
|
|
|
|
|
|
|
describe 'POST retry.json' do
|
|
|
|
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
|
|
|
|
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
post :retry, namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
|
|
|
id: pipeline.id,
|
|
|
|
format: :json
|
|
|
|
end
|
|
|
|
|
2017-06-07 10:32:16 +00:00
|
|
|
context 'when builds are enabled' do
|
|
|
|
let(:feature) { ProjectFeature::ENABLED }
|
2017-07-03 21:58:28 +00:00
|
|
|
|
2017-06-07 10:32:16 +00:00
|
|
|
it 'retries a pipeline without returning any content' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:no_content)
|
2017-06-07 10:32:16 +00:00
|
|
|
expect(build.reload).to be_retried
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when builds are disabled' do
|
|
|
|
it 'fails to retry pipeline' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
2017-06-07 10:32:16 +00:00
|
|
|
end
|
2017-05-04 19:01:15 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'POST cancel.json' do
|
|
|
|
let!(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
|
2017-07-03 21:58:28 +00:00
|
|
|
|
2017-05-04 19:01:15 +00:00
|
|
|
before do
|
|
|
|
post :cancel, namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
|
|
|
id: pipeline.id,
|
|
|
|
format: :json
|
|
|
|
end
|
|
|
|
|
2017-06-07 10:32:16 +00:00
|
|
|
context 'when builds are enabled' do
|
|
|
|
let(:feature) { ProjectFeature::ENABLED }
|
2017-07-03 21:58:28 +00:00
|
|
|
|
2017-06-07 10:32:16 +00:00
|
|
|
it 'cancels a pipeline without returning any content' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:no_content)
|
2017-06-07 10:32:16 +00:00
|
|
|
expect(pipeline.reload).to be_canceled
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when builds are disabled' do
|
|
|
|
it 'fails to retry pipeline' do
|
2017-10-19 18:28:19 +00:00
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
2017-06-07 10:32:16 +00:00
|
|
|
end
|
2017-05-04 19:01:15 +00:00
|
|
|
end
|
|
|
|
end
|
2016-12-20 14:53:53 +00:00
|
|
|
end
|