387 lines
12 KiB
Ruby
387 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe 'getting pipeline information nested in a project' do
|
|
include GraphqlHelpers
|
|
|
|
let_it_be(:project) { create(:project, :repository, :public) }
|
|
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
|
let_it_be(:current_user) { create(:user) }
|
|
let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline, stage_idx: 0, stage: 'build') }
|
|
let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline, stage_idx: 0, stage: 'build') }
|
|
let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline, stage_idx: 0, stage: 'build') }
|
|
|
|
let(:path) { %i[project pipeline] }
|
|
let(:pipeline_graphql_data) { graphql_data_at(*path) }
|
|
let(:depth) { 3 }
|
|
let(:excluded) { %w[job project] } # Project is very expensive, due to the number of fields
|
|
let(:fields) { all_graphql_fields_for('Pipeline', excluded: excluded, max_depth: depth) }
|
|
|
|
let(:query) do
|
|
graphql_query_for(
|
|
:project,
|
|
{ full_path: project.full_path },
|
|
query_graphql_field(:pipeline, { iid: pipeline.iid.to_s }, fields)
|
|
)
|
|
end
|
|
|
|
it_behaves_like 'a working graphql query', :use_clean_rails_memory_store_caching, :request_store do
|
|
before do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
end
|
|
|
|
it 'contains pipeline information' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(pipeline_graphql_data).not_to be_nil
|
|
end
|
|
|
|
it 'contains configSource' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(pipeline_graphql_data['configSource']).to eq('UNKNOWN_SOURCE')
|
|
end
|
|
|
|
context 'when batching' do
|
|
let!(:pipeline2) { successful_pipeline }
|
|
let!(:pipeline3) { successful_pipeline }
|
|
let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
|
|
|
|
def successful_pipeline
|
|
create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)])
|
|
end
|
|
|
|
it 'executes the finder once' do
|
|
mock = double(Ci::PipelinesFinder)
|
|
opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
|
|
|
|
expect(Ci::PipelinesFinder).to receive(:new).once.with(project, current_user, opts).and_return(mock)
|
|
expect(mock).to receive(:execute).once.and_return(Ci::Pipeline.none)
|
|
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
it 'keeps the queries under the threshold' do
|
|
control = ActiveRecord::QueryRecorder.new do
|
|
single_pipeline_query = build_query_to_find_pipeline_shas(pipeline)
|
|
|
|
post_graphql(single_pipeline_query, current_user: current_user)
|
|
end
|
|
|
|
aggregate_failures do
|
|
expect(response).to have_gitlab_http_status(:success)
|
|
expect do
|
|
post_graphql(query, current_user: current_user)
|
|
end.not_to exceed_query_limit(control)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when enough data is requested' do
|
|
let(:fields) do
|
|
query_graphql_field(:jobs, nil,
|
|
query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3)))
|
|
end
|
|
|
|
it 'contains jobs' do
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly(
|
|
a_graphql_entity_for(
|
|
build_job, :name, :duration,
|
|
'status' => build_job.status.upcase
|
|
),
|
|
a_graphql_entity_for(
|
|
failed_build,
|
|
'status' => failed_build.status.upcase
|
|
),
|
|
a_graphql_entity_for(
|
|
bridge,
|
|
'status' => bridge.status.upcase
|
|
)
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when requesting only builds with certain statuses' do
|
|
let(:variables) do
|
|
{
|
|
path: project.full_path,
|
|
pipelineIID: pipeline.iid.to_s,
|
|
status: :FAILED
|
|
}
|
|
end
|
|
|
|
let(:query) do
|
|
<<~GQL
|
|
query($path: ID!, $pipelineIID: ID!, $status: CiJobStatus!) {
|
|
project(fullPath: $path) {
|
|
pipeline(iid: $pipelineIID) {
|
|
jobs(statuses: [$status]) {
|
|
nodes {
|
|
#{all_graphql_fields_for('CiJob', max_depth: 1)}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GQL
|
|
end
|
|
|
|
it 'can filter build jobs by status' do
|
|
post_graphql(query, current_user: current_user, variables: variables)
|
|
|
|
expect(graphql_data_at(*path, :jobs, :nodes))
|
|
.to contain_exactly(a_graphql_entity_for(failed_build))
|
|
end
|
|
end
|
|
|
|
context 'when requesting a specific job' do
|
|
let(:variables) do
|
|
{
|
|
path: project.full_path,
|
|
pipelineIID: pipeline.iid.to_s
|
|
}
|
|
end
|
|
|
|
let(:build_fields) do
|
|
all_graphql_fields_for('CiJob', max_depth: 1)
|
|
end
|
|
|
|
let(:query) do
|
|
<<~GQL
|
|
query($path: ID!, $pipelineIID: ID!, $jobName: String, $jobID: JobID) {
|
|
project(fullPath: $path) {
|
|
pipeline(iid: $pipelineIID) {
|
|
job(id: $jobID, name: $jobName) {
|
|
#{build_fields}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GQL
|
|
end
|
|
|
|
let(:the_job) do
|
|
a_graphql_entity_for(build_job, :name)
|
|
end
|
|
|
|
it 'can request a build by name' do
|
|
vars = variables.merge(jobName: build_job.name)
|
|
|
|
post_graphql(query, current_user: current_user, variables: vars)
|
|
|
|
expect(graphql_data_at(*path, :job)).to match(the_job)
|
|
end
|
|
|
|
it 'can request a build by ID' do
|
|
vars = variables.merge(jobID: global_id_of(build_job))
|
|
|
|
post_graphql(query, current_user: current_user, variables: vars)
|
|
|
|
expect(graphql_data_at(*path, :job)).to match(the_job)
|
|
end
|
|
|
|
context 'when we request nested fields of the build' do
|
|
let_it_be(:needy) { create(:ci_build, :dependent, pipeline: pipeline) }
|
|
|
|
let(:build_fields) { 'needs { nodes { name } }' }
|
|
let(:vars) { variables.merge(jobID: global_id_of(needy)) }
|
|
|
|
it 'returns the nested data' do
|
|
post_graphql(query, current_user: current_user, variables: vars)
|
|
|
|
expect(graphql_data_at(*path, :job, :needs, :nodes)).to contain_exactly(
|
|
a_hash_including('name' => needy.needs.first.name)
|
|
)
|
|
end
|
|
|
|
it 'requires a constant number of queries' do
|
|
fst_user = create(:user)
|
|
snd_user = create(:user)
|
|
path = %i[project pipeline job needs nodes name]
|
|
|
|
baseline = ActiveRecord::QueryRecorder.new do
|
|
post_graphql(query, current_user: fst_user, variables: vars)
|
|
end
|
|
|
|
expect(baseline.count).to be > 0
|
|
dep_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
|
|
|
|
deps = create_list(:ci_build, 3, :unique_name, pipeline: pipeline)
|
|
deps.each { |d| create(:ci_build_need, build: needy, name: d.name) }
|
|
|
|
expect do
|
|
post_graphql(query, current_user: snd_user, variables: vars)
|
|
end.not_to exceed_query_limit(baseline)
|
|
|
|
more_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
|
|
|
|
expect(more_names).to include(*dep_names)
|
|
expect(more_names.count).to be > dep_names.count
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when requesting a specific test suite' do
|
|
let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
|
|
let(:suite_name) { 'test' }
|
|
let_it_be(:build_ids) { pipeline.latest_builds.pluck(:id) }
|
|
|
|
let(:variables) do
|
|
{
|
|
path: project.full_path,
|
|
pipelineIID: pipeline.iid.to_s
|
|
}
|
|
end
|
|
|
|
let(:query) do
|
|
<<~GQL
|
|
query($path: ID!, $pipelineIID: ID!, $buildIds: [ID!]!) {
|
|
project(fullPath: $path) {
|
|
pipeline(iid: $pipelineIID) {
|
|
testSuite(buildIds: $buildIds) {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GQL
|
|
end
|
|
|
|
it 'can request a test suite by an array of build_ids' do
|
|
vars = variables.merge(buildIds: build_ids)
|
|
|
|
post_graphql(query, current_user: current_user, variables: vars)
|
|
|
|
expect(graphql_data_at(:project, :pipeline, :testSuite, :name)).to eq(suite_name)
|
|
end
|
|
|
|
context 'when pipeline has no builds that matches the given build_ids' do
|
|
let_it_be(:build_ids) { [non_existing_record_id] }
|
|
|
|
it 'returns nil' do
|
|
vars = variables.merge(buildIds: build_ids)
|
|
|
|
post_graphql(query, current_user: current_user, variables: vars)
|
|
|
|
expect(graphql_data_at(*path, :test_suite)).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'N+1 queries on pipeline jobs' do
|
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
|
|
|
let(:fields) do
|
|
<<~FIELDS
|
|
jobs {
|
|
nodes {
|
|
previousStageJobsAndNeeds {
|
|
nodes {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
FIELDS
|
|
end
|
|
|
|
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
|
|
build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline)
|
|
test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline)
|
|
create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage)
|
|
create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage)
|
|
create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage)
|
|
test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage)
|
|
create(:ci_build_need, build: test_job, name: 'docker 1 2')
|
|
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline)
|
|
test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline)
|
|
create(:ci_build_need, build: test_b_job, name: 'docker 2 2')
|
|
|
|
expect do
|
|
post_graphql(query, current_user: current_user)
|
|
end.not_to exceed_all_query_limit(control)
|
|
end
|
|
end
|
|
|
|
context 'N+1 queries on stages jobs' do
|
|
let(:depth) { 5 }
|
|
let(:fields) do
|
|
<<~FIELDS
|
|
stages {
|
|
nodes {
|
|
name
|
|
groups {
|
|
nodes {
|
|
name
|
|
jobs {
|
|
nodes {
|
|
name
|
|
needs {
|
|
nodes {
|
|
name
|
|
}
|
|
}
|
|
status: detailedStatus {
|
|
tooltip
|
|
hasDetails
|
|
detailsPath
|
|
action {
|
|
buttonTitle
|
|
path
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
FIELDS
|
|
end
|
|
|
|
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
|
|
# create extra statuses
|
|
create(:generic_commit_status, :pending, name: 'generic-build-a', pipeline: pipeline, stage_idx: 0, stage: 'build')
|
|
create(:ci_bridge, :failed, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
|
|
|
# warm up
|
|
post_graphql(query, current_user: current_user)
|
|
|
|
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
|
post_graphql(query, current_user: current_user)
|
|
end
|
|
|
|
create(:generic_commit_status, :pending, name: 'generic-build-b', pipeline: pipeline, stage_idx: 0, stage: 'build')
|
|
create(:ci_build, :failed, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
|
create(:ci_build, :running, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
|
|
create(:ci_build, :pending, name: 'deploy-b', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
|
create(:ci_bridge, :failed, name: 'deploy-c', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
|
|
|
|
expect do
|
|
post_graphql(query, current_user: current_user)
|
|
end.not_to exceed_all_query_limit(control)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def build_query_to_find_pipeline_shas(*pipelines)
|
|
pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
|
|
"pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
|
|
end.join(' ')
|
|
|
|
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
|
|
end
|
|
end
|