diff options
Diffstat (limited to 'spec/requests/api/graphql/ci/runner_spec.rb')
-rw-r--r-- | spec/requests/api/graphql/ci/runner_spec.rb | 264 |
1 files changed, 192 insertions, 72 deletions
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 94c0a3c41bd..ca08e780758 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.runner(id)' do +RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do include GraphqlHelpers let_it_be(:user) { create(:user, :admin) } @@ -74,34 +74,39 @@ RSpec.describe 'Query.runner(id)' do runner_data = graphql_data_at(:runner) expect(runner_data).not_to be_nil - expect(runner_data).to match a_hash_including( - 'id' => runner.to_global_id.to_s, - 'description' => runner.description, - 'createdAt' => runner.created_at&.iso8601, - 'contactedAt' => runner.contacted_at&.iso8601, - 'version' => runner.version, - 'shortSha' => runner.short_sha, - 'revision' => runner.revision, - 'locked' => false, - 'active' => runner.active, - 'paused' => !runner.active, - 'status' => runner.status('14.5').to_s.upcase, - 'maximumTimeout' => runner.maximum_timeout, - 'accessLevel' => runner.access_level.to_s.upcase, - 'runUntagged' => runner.run_untagged, - 'ipAddress' => runner.ip_address, - 'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE', - 'executorName' => runner.executor_type&.dasherize, - 'architectureName' => runner.architecture, - 'platformName' => runner.platform, - 'maintenanceNote' => runner.maintenance_note, - 'maintenanceNoteHtml' => + expect(runner_data).to match a_graphql_entity_for( + runner, + description: runner.description, + created_at: runner.created_at&.iso8601, + contacted_at: runner.contacted_at&.iso8601, + version: runner.version, + short_sha: runner.short_sha, + revision: runner.revision, + locked: false, + active: runner.active, + paused: !runner.active, + status: runner.status('14.5').to_s.upcase, + job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE', + maximum_timeout: runner.maximum_timeout, + access_level: runner.access_level.to_s.upcase, + run_untagged: runner.run_untagged, + ip_address: runner.ip_address, + runner_type: runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE', + executor_name: runner.executor_type&.dasherize, + architecture_name: runner.architecture, + platform_name: runner.platform, + maintenance_note: runner.maintenance_note, + maintenance_note_html: runner.maintainer_note.present? ? a_string_including('<strong>Test maintenance note</strong>') : '', - 'jobCount' => 0, - 'jobs' => a_hash_including("count" => 0, "nodes" => [], "pageInfo" => anything), - 'projectCount' => nil, - 'adminUrl' => "http://localhost/admin/runners/#{runner.id}", - 'userPermissions' => { + job_count: runner.builds.count, + jobs: a_hash_including( + "count" => runner.builds.count, + "nodes" => an_instance_of(Array), + "pageInfo" => anything + ), + project_count: nil, + admin_url: "http://localhost/admin/runners/#{runner.id}", + user_permissions: { 'readRunner' => true, 'updateRunner' => true, 'deleteRunner' => true, @@ -129,10 +134,7 @@ RSpec.describe 'Query.runner(id)' do runner_data = graphql_data_at(:runner) expect(runner_data).not_to be_nil - expect(runner_data).to match a_hash_including( - 'id' => runner.to_global_id.to_s, - 'adminUrl' => nil - ) + expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil) expect(runner_data['tagList']).to match_array runner.tag_list end end @@ -179,6 +181,16 @@ RSpec.describe 'Query.runner(id)' do expect(runner_data).not_to include('tagList') end end + + context 'with build running' do + before do + project = create(:project, :repository) + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, :running, runner: runner, pipeline: pipeline) + end + + it_behaves_like 'runner details fetch' + end end describe 'for project runner' do @@ -216,9 +228,47 @@ RSpec.describe 'Query.runner(id)' do runner_data = graphql_data_at(:runner) - expect(runner_data).to match a_hash_including( - 'id' => project_runner.to_global_id.to_s, - 'locked' => is_locked + expect(runner_data).to match a_graphql_entity_for(project_runner, locked: is_locked) + end + end + end + + describe 'jobCount' do + let_it_be(:pipeline1) { create(:ci_pipeline, project: project1) } + let_it_be(:pipeline2) { create(:ci_pipeline, project: project1) } + let_it_be(:build1) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline1) } + let_it_be(:build2) { create(:ci_build, :running, runner: active_project_runner, pipeline: pipeline2) } + + let(:runner_query_fragment) { 'id jobCount' } + let(:query) do + %( + query { + runner1: runner(id: "#{active_project_runner.to_global_id}") { #{runner_query_fragment} } + runner2: runner(id: "#{inactive_instance_runner.to_global_id}") { #{runner_query_fragment} } + } + ) + end + + it 'retrieves correct jobCount values' do + post_graphql(query, current_user: user) + + expect(graphql_data).to match a_hash_including( + 'runner1' => a_graphql_entity_for(active_project_runner, job_count: 2), + 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0) + ) + end + + context 'when JOB_COUNT_LIMIT is in effect' do + before do + stub_const('Types::Ci::RunnerType::JOB_COUNT_LIMIT', 0) + end + + it 'retrieves correct capped jobCount values' do + post_graphql(query, current_user: user) + + expect(graphql_data).to match a_hash_including( + 'runner1' => a_graphql_entity_for(active_project_runner, job_count: 1), + 'runner2' => a_graphql_entity_for(inactive_instance_runner, job_count: 0) ) end end @@ -243,18 +293,8 @@ RSpec.describe 'Query.runner(id)' do post_graphql(query, current_user: user) expect(graphql_data).to match a_hash_including( - 'runner1' => { - 'id' => runner1.to_global_id.to_s, - 'ownerProject' => { - 'id' => project2.to_global_id.to_s - } - }, - 'runner2' => { - 'id' => runner2.to_global_id.to_s, - 'ownerProject' => { - 'id' => project1.to_global_id.to_s - } - } + 'runner1' => a_graphql_entity_for(runner1, owner_project: a_graphql_entity_for(project2)), + 'runner2' => a_graphql_entity_for(runner2, owner_project: a_graphql_entity_for(project1)) ) end end @@ -284,8 +324,8 @@ RSpec.describe 'Query.runner(id)' do it 'retrieves groups field with expected value' do post_graphql(query, current_user: user) - runner_data = graphql_data_at(:runner, :groups) - expect(runner_data).to eq 'nodes' => [{ 'id' => group.to_global_id.to_s }] + runner_data = graphql_data_at(:runner, :groups, :nodes) + expect(runner_data).to contain_exactly(a_graphql_entity_for(group)) end end @@ -409,13 +449,13 @@ RSpec.describe 'Query.runner(id)' do 'jobCount' => 1, 'jobs' => a_hash_including( "count" => 1, - "nodes" => [{ "id" => job.to_global_id.to_s, "status" => job.status.upcase }] + "nodes" => [a_graphql_entity_for(job, status: job.status.upcase)] ), 'projectCount' => 2, 'projects' => { 'nodes' => [ - { 'id' => project1.to_global_id.to_s }, - { 'id' => project2.to_global_id.to_s } + a_graphql_entity_for(project1), + a_graphql_entity_for(project2) ] }) expect(runner2_data).to match a_hash_including( @@ -486,15 +526,24 @@ RSpec.describe 'Query.runner(id)' do groups { nodes { id + path + fullPath + webUrl } } projects { nodes { id + path + fullPath + webUrl } } ownerProject { id + path + fullPath + webUrl } } SINGLE @@ -503,8 +552,8 @@ RSpec.describe 'Query.runner(id)' do let(:active_project_runner2) { create(:ci_runner, :project) } let(:active_group_runner2) { create(:ci_runner, :group) } - # Currently excluding known N+1 issues, see https://gitlab.com/gitlab-org/gitlab/-/issues/334759 - let(:excluded_fields) { %w[jobCount groups projects ownerProject] } + # Exclude fields that are already hardcoded above + let(:excluded_fields) { %w[jobs groups projects ownerProject] } let(:single_query) do <<~QUERY @@ -542,27 +591,98 @@ RSpec.describe 'Query.runner(id)' do expect(graphql_data.count).to eq 6 expect(graphql_data).to match( a_hash_including( - 'instance_runner1' => a_hash_including('id' => active_instance_runner.to_global_id.to_s), - 'instance_runner2' => a_hash_including('id' => inactive_instance_runner.to_global_id.to_s), - 'group_runner1' => a_hash_including( - 'id' => active_group_runner.to_global_id.to_s, - 'groups' => { 'nodes' => [a_hash_including('id' => group.to_global_id.to_s)] } + 'instance_runner1' => a_graphql_entity_for(active_instance_runner), + 'instance_runner2' => a_graphql_entity_for(inactive_instance_runner), + 'group_runner1' => a_graphql_entity_for( + active_group_runner, + groups: { 'nodes' => contain_exactly(a_graphql_entity_for(group)) } ), - 'group_runner2' => a_hash_including( - 'id' => active_group_runner2.to_global_id.to_s, - 'groups' => { 'nodes' => [a_hash_including('id' => active_group_runner2.groups[0].to_global_id.to_s)] } + 'group_runner2' => a_graphql_entity_for( + active_group_runner2, + groups: { 'nodes' => active_group_runner2.groups.map { |g| a_graphql_entity_for(g) } } ), - 'project_runner1' => a_hash_including( - 'id' => active_project_runner.to_global_id.to_s, - 'projects' => { 'nodes' => [a_hash_including('id' => active_project_runner.projects[0].to_global_id.to_s)] }, - 'ownerProject' => a_hash_including('id' => active_project_runner.projects[0].to_global_id.to_s) + 'project_runner1' => a_graphql_entity_for( + active_project_runner, + projects: { 'nodes' => active_project_runner.projects.map { |p| a_graphql_entity_for(p) } }, + owner_project: a_graphql_entity_for(active_project_runner.projects[0]) ), - 'project_runner2' => a_hash_including( - 'id' => active_project_runner2.to_global_id.to_s, - 'projects' => { - 'nodes' => [a_hash_including('id' => active_project_runner2.projects[0].to_global_id.to_s)] - }, - 'ownerProject' => a_hash_including('id' => active_project_runner2.projects[0].to_global_id.to_s) + 'project_runner2' => a_graphql_entity_for( + active_project_runner2, + projects: { 'nodes' => active_project_runner2.projects.map { |p| a_graphql_entity_for(p) } }, + owner_project: a_graphql_entity_for(active_project_runner2.projects[0]) + ) + )) + end + end + + describe 'Query limits with jobs' do + let!(:group1) { create(:group) } + let!(:group2) { create(:group) } + let!(:project1) { create(:project, :repository, group: group1) } + let!(:project2) { create(:project, :repository, group: group1) } + let!(:project3) { create(:project, :repository, group: group2) } + + let!(:merge_request1) { create(:merge_request, source_project: project1) } + let!(:merge_request2) { create(:merge_request, source_project: project3) } + + let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) } + let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) } + let!(:pipeline1) do + create(:ci_pipeline, project: project1, source: :merge_request_event, merge_request: merge_request1, ref: 'main', + target_sha: 'xxx') + end + + let(:query) do + <<~QUERY + { + runner(id: "#{project_runner2.to_global_id}") { + id + jobs { + nodes { + id + detailedStatus { + id + detailsPath + group + icon + text + } + shortSha + commitPath + finishedAt + duration + queuedDuration + tags + } + } + } + } + QUERY + end + + it 'does not execute more queries per job', :aggregate_failures do + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + + control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) } + + # Add a new build to project_runner2 + project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3) + pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2, + ref: 'main', target_sha: 'xxx') + build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2) + + args[:current_user] = create(:user, :admin) # do not reuse same user + expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + + expect(graphql_data.count).to eq 1 + expect(graphql_data).to match( + a_hash_including( + 'runner' => a_graphql_entity_for( + project_runner2, + jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) } ) )) end |