Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class PrepareBuildService
    attr_reader :build

    def initialize(build)
      @build = build
    end

    def execute
      prerequisites.each(&:complete!)

      build.enqueue_preparing!
    rescue StandardError => e
      Gitlab::ErrorTracking.track_exception(e, build_id: build.id)

      build.drop(:unmet_prerequisites)
    end

    private

    def prerequisites
      build.prerequisites
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PrepareBuildService, feature_category: :continuous_integration do
  describe '#execute' do
    let(:build) { create(:ci_build, :preparing) }

    subject { described_class.new(build).execute }

    before do
      allow(build).to receive(:prerequisites).and_return(prerequisites)
    end

    shared_examples 'build enqueueing' do
      it 'enqueues the build' do
        expect(build).to receive(:enqueue_preparing).once

        subject
      end
    end

    context 'build has unmet prerequisites' do
      let(:prerequisite) { double(complete!: true) }
      let(:prerequisites) { [prerequisite] }

      it 'completes each prerequisite' do
        expect(prerequisites).to all(receive(:complete!))

        subject
      end

      include_examples 'build enqueueing'

      context 'prerequisites fail to complete' do
        before do
          allow(build).to receive(:enqueue_preparing).and_return(false)
        end

        it 'drops the build' do
          expect(build).to receive(:drop).with(:unmet_prerequisites).once

          subject
        end
      end

      context 'prerequisites raise an error' do
        before do
          allow(prerequisite).to receive(:complete!).and_raise Kubeclient::HttpError.new(401, 'unauthorized', nil)
        end

        it 'drops the build and notifies Sentry' do
          expect(build).to receive(:drop).with(:unmet_prerequisites).once
          expect(Gitlab::ErrorTracking).to receive(:track_exception)
            .with(instance_of(Kubeclient::HttpError), hash_including(build_id: build.id))

          subject
        end
      end
    end

    context 'build has no prerequisites' do
      let(:prerequisites) { [] }

      include_examples 'build enqueueing'
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class PipelineBridgeStatusService < ::BaseService
    def execute(pipeline)
      return unless pipeline.bridge_triggered?

      begin
        pipeline.source_bridge.inherit_status_from_downstream!(pipeline)
      rescue StateMachines::InvalidTransition => e
        Gitlab::ErrorTracking.track_exception(
          Ci::Bridge::InvalidTransitionError.new(e.message),
          bridge_id: pipeline.source_bridge.id,
          downstream_pipeline_id: pipeline.id)
      end
    end
  end
end

Ci::PipelineBridgeStatusService.prepend_mod_with('Ci::PipelineBridgeStatusService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineBridgeStatusService, feature_category: :continuous_integration do
  let(:user) { build(:user) }
  let_it_be(:project) { create(:project) }

  let(:pipeline) { build(:ci_pipeline, project: project) }

  describe '#execute' do
    subject { described_class.new(project, user).execute(pipeline) }

    context 'when pipeline has upstream bridge' do
      let(:bridge) { build(:ci_bridge) }

      before do
        pipeline.source_bridge = bridge
      end

      it 'calls inherit_status_from_downstream on upstream bridge' do
        expect(bridge).to receive(:inherit_status_from_downstream!).with(pipeline)

        subject
      end

      context 'when bridge job status raises state machine errors' do
        before do
          pipeline.drop!
          bridge.drop!
        end

        it 'tracks the exception' do
          expect(Gitlab::ErrorTracking)
            .to receive(:track_exception)
            .with(
              instance_of(Ci::Bridge::InvalidTransitionError),
              bridge_id: bridge.id,
              downstream_pipeline_id: pipeline.id)

          subject
        end
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  # This class responsible for assigning
  # proper pending build to runner on runner API request
  class RegisterJobService
    include ::Gitlab::Ci::Artifacts::Logger

    attr_reader :runner, :runner_manager, :metrics

    TEMPORARY_LOCK_TIMEOUT = 3.seconds

    Result = Struct.new(:build, :build_json, :build_presented, :valid?)

    ##
    # The queue depth limit number has been determined by observing 95
    # percentile of effective queue depth on gitlab.com. This is only likely to
    # affect 5% of the worst case scenarios.
    MAX_QUEUE_DEPTH = 45

    def initialize(runner, runner_manager)
      @runner = runner
      @runner_manager = runner_manager
      @metrics = ::Gitlab::Ci::Queue::Metrics.new(runner)
    end

    def execute(params = {})
      replica_caught_up =
        ::Ci::Runner.sticking.find_caught_up_replica(:runner, runner.id, use_primary_on_failure: false)

      @metrics.increment_queue_operation(:queue_attempt)

      result = @metrics.observe_queue_time(:process, @runner.runner_type) do
        process_queue(params)
      end

      # Since we execute this query against replica it might lead to false-positive
      # We might receive the positive response: "hi, we don't have any more builds for you".
      # This might not be true. If our DB replica is not up-to date with when runner event was generated
      # we might still have some CI builds to be picked. Instead we should say to runner:
      # "Hi, we don't have any more builds now,  but not everything is right anyway, so try again".
      # Runner will retry, but again, against replica, and again will check if replication lag did catch-up.
      if !replica_caught_up && !result.build
        metrics.increment_queue_operation(:queue_replication_lag)

        ::Ci::RegisterJobService::Result.new(nil, nil, nil, false) # rubocop:disable Cop/AvoidReturnFromBlocks
      else
        result
      end
    end

    private

    def process_queue(params)
      valid = true
      depth = 0

      each_build(params) do |build|
        depth += 1
        @metrics.increment_queue_operation(:queue_iteration)

        if depth > max_queue_depth
          @metrics.increment_queue_operation(:queue_depth_limit)

          valid = false

          break
        end

        # We read builds from replicas
        # It is likely that some other concurrent connection is processing
        # a given build at a given moment. To avoid an expensive compute
        # we perform an exclusive lease on Redis to acquire a build temporarily
        unless acquire_temporary_lock(build.id)
          @metrics.increment_queue_operation(:build_temporary_locked)

          # We failed to acquire lock
          # - our queue is not complete as some resources are locked temporarily
          # - we need to re-process it again to ensure that all builds are handled
          valid = false

          next
        end

        result = process_build(build, params)
        next unless result

        if result.valid?
          @metrics.register_success(result.build_presented)
          @metrics.observe_queue_depth(:found, depth)

          return result # rubocop:disable Cop/AvoidReturnFromBlocks
        else
          # The usage of valid: is described in
          # handling of ActiveRecord::StaleObjectError
          valid = false
        end
      end

      @metrics.increment_queue_operation(:queue_conflict) unless valid
      @metrics.observe_queue_depth(:conflict, depth) unless valid
      @metrics.observe_queue_depth(:not_found, depth) if valid
      @metrics.register_failure

      Result.new(nil, nil, nil, valid)
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def each_build(params, &blk)
      queue = ::Ci::Queue::BuildQueueService.new(runner)

      builds = if runner.instance_type?
                 queue.builds_for_shared_runner
               elsif runner.group_type?
                 queue.builds_for_group_runner
               else
                 queue.builds_for_project_runner
               end

      if runner.ref_protected?
        builds = queue.builds_for_protected_runner(builds)
      end

      # pick builds that does not have other tags than runner's one
      builds = queue.builds_matching_tag_ids(builds, runner.tags.ids)

      # pick builds that have at least one tag
      unless runner.run_untagged?
        builds = queue.builds_with_any_tags(builds)
      end

      build_ids = retrieve_queue(-> { queue.execute(builds) })

      @metrics.observe_queue_size(-> { build_ids.size }, @runner.runner_type)

      build_ids.each { |build_id| yield Ci::Build.find(build_id) }
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def retrieve_queue(queue_query_proc)
      ##
      # We want to reset a load balancing session to discard the side
      # effects of writes that could have happened prior to this moment.
      #
      ::Gitlab::Database::LoadBalancing::Session.clear_session

      @metrics.observe_queue_time(:retrieve, @runner.runner_type) do
        queue_query_proc.call
      end
    end

    def process_build(build, params)
      unless build.pending?
        @metrics.increment_queue_operation(:build_not_pending)

        ##
        # If this build can not be picked because we had stale data in
        # `ci_pending_builds` table, we need to respond with 409 to retry
        # this operation.
        #
        if ::Ci::UpdateBuildQueueService.new.remove!(build)
          return Result.new(nil, nil, nil, false)
        end

        return
      end

      if runner.matches_build?(build)
        @metrics.increment_queue_operation(:build_can_pick)
      else
        @metrics.increment_queue_operation(:build_not_pick)

        return
      end

      # In case when 2 runners try to assign the same build, second runner will be declined
      # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
      if assign_runner!(build, params)
        present_build!(build)
      end
    rescue ActiveRecord::StaleObjectError
      # We are looping to find another build that is not conflicting
      # It also indicates that this build can be picked and passed to runner.
      # If we don't do it, basically a bunch of runners would be competing for a build
      # and thus we will generate a lot of 409. This will increase
      # the number of generated requests, also will reduce significantly
      # how many builds can be picked by runner in a unit of time.
      # In case we hit the concurrency-access lock,
      # we still have to return 409 in the end,
      # to make sure that this is properly handled by runner.
      @metrics.increment_queue_operation(:build_conflict_lock)

      Result.new(nil, nil, nil, false)
    rescue StateMachines::InvalidTransition
      @metrics.increment_queue_operation(:build_conflict_transition)

      Result.new(nil, nil, nil, false)
    rescue StandardError => ex
      @metrics.increment_queue_operation(:build_conflict_exception)

      # If an error (e.g. GRPC::DeadlineExceeded) occurred constructing
      # the result, consider this as a failure to be retried.
      scheduler_failure!(build)
      track_exception_for_build(ex, build)

      # skip, and move to next one
      nil
    end

    def max_queue_depth
      MAX_QUEUE_DEPTH
    end

    # Force variables evaluation to occur now
    def present_build!(build)
      # We need to use the presenter here because Gitaly calls in the presenter
      # may fail, and we need to ensure the response has been generated.
      presented_build = ::Ci::BuildRunnerPresenter.new(build) # rubocop:disable CodeReuse/Presenter

      log_artifacts_context(build)
      log_build_dependencies_size(presented_build)

      build_json = Gitlab::Json.dump(::API::Entities::Ci::JobRequest::Response.new(presented_build))
      Result.new(build, build_json, presented_build, true)
    end

    def log_build_dependencies_size(presented_build)
      return unless ::Feature.enabled?(:ci_build_dependencies_artifacts_logger, type: :ops)

      presented_build.all_dependencies.then do |dependencies|
        size = dependencies.sum do |build|
          build.available_artifacts? ? build.artifacts_file.size : 0
        end

        log_build_dependencies(size: size, count: dependencies.size) if size > 0
      end
    end

    def assign_runner!(build, params)
      build.runner_id = runner.id
      build.runner_session_attributes = params[:session] if params[:session].present?

      failure_reason, _ = pre_assign_runner_checks.find { |_, check| check.call(build, params) }

      if failure_reason
        @metrics.increment_queue_operation(:runner_pre_assign_checks_failed)

        build.drop!(failure_reason)
      else
        @metrics.increment_queue_operation(:runner_pre_assign_checks_success)

        build.run!
        build.runner_manager = runner_manager if runner_manager
      end

      !failure_reason
    end

    def acquire_temporary_lock(build_id)
      return true if Feature.disabled?(:ci_register_job_temporary_lock, runner, type: :ops)

      key = "build/register/#{build_id}"

      Gitlab::ExclusiveLease
        .new(key, timeout: TEMPORARY_LOCK_TIMEOUT.to_i)
        .try_obtain
    end

    def scheduler_failure!(build)
      Gitlab::OptimisticLocking.retry_lock(build, 3, name: 'register_job_scheduler_failure') do |subject|
        subject.drop!(:scheduler_failure)
      end
    rescue StandardError => ex
      build.doom!

      # This requires extra exception, otherwise we would loose information
      # why we cannot perform `scheduler_failure`
      track_exception_for_build(ex, build)
    end

    def track_exception_for_build(ex, build)
      Gitlab::ErrorTracking.track_exception(ex,
        build_id: build.id,
        build_name: build.name,
        build_stage: build.stage_name,
        pipeline_id: build.pipeline_id,
        project_id: build.project_id
      )
    end

    def pre_assign_runner_checks
      {
        missing_dependency_failure: -> (build, _) { !build.has_valid_build_dependencies? },
        runner_unsupported: -> (build, params) { !build.supported_runner?(params.dig(:info, :features)) },
        archived_failure: -> (build, _) { build.archived? },
        project_deleted: -> (build, _) { build.project.pending_delete? },
        builds_disabled: -> (build, _) { !build.project.builds_enabled? },
        user_blocked: -> (build, _) { build.user&.blocked? }
      }
    end
  end
end

Ci::RegisterJobService.prepend_mod_with('Ci::RegisterJobService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

module Ci
  RSpec.describe RegisterJobService, feature_category: :continuous_integration do
    let_it_be(:group) { create(:group) }
    let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
    let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }

    let_it_be(:shared_runner) { create(:ci_runner, :instance) }
    let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
    let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
    let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

    describe '#execute' do
      subject(:execute) { described_class.new(runner, runner_manager).execute }

      let(:runner_manager) { nil }

      context 'checks database loadbalancing stickiness' do
        let(:runner) { shared_runner }

        before do
          project.update!(shared_runners_enabled: false)
        end

        it 'result is valid if replica did caught-up', :aggregate_failures do
          expect(ApplicationRecord.sticking).to receive(:find_caught_up_replica)
            .with(:runner, runner.id, use_primary_on_failure: false)
            .and_return(true)

          expect { execute }.not_to change { Ci::RunnerManagerBuild.count }.from(0)
          expect(execute).to be_valid
          expect(execute.build).to be_nil
          expect(execute.build_json).to be_nil
        end

        it 'result is invalid if replica did not caught-up', :aggregate_failures do
          expect(ApplicationRecord.sticking).to receive(:find_caught_up_replica)
            .with(:runner, shared_runner.id, use_primary_on_failure: false)
            .and_return(false)

          expect(subject).not_to be_valid
          expect(subject.build).to be_nil
          expect(subject.build_json).to be_nil
        end
      end

      shared_examples 'handles runner assignment' do
        context 'runner follows tag list' do
          subject(:build) { build_on(project_runner, runner_manager: project_runner_manager) }

          let(:project_runner_manager) { nil }

          context 'when job has tag' do
            before do
              pending_job.update!(tag_list: ["linux"])
              pending_job.reload
              pending_job.create_queuing_entry!
            end

            context 'and runner has matching tag' do
              before do
                project_runner.update!(tag_list: ["linux"])
              end

              context 'with no runner manager specified' do
                it 'picks build' do
                  expect(build).to eq(pending_job)
                  expect(pending_job.runner_manager).to be_nil
                end
              end

              context 'with runner manager specified' do
                let(:project_runner_manager) { create(:ci_runner_machine, runner: project_runner) }

                it 'picks build and assigns runner manager' do
                  expect(build).to eq(pending_job)
                  expect(pending_job.runner_manager).to eq(project_runner_manager)
                end
              end
            end

            it 'does not pick build with different tag' do
              project_runner.update!(tag_list: ["win32"])
              expect(build).to be_falsey
            end

            it 'does not pick build with tag' do
              pending_job.create_queuing_entry!
              expect(build).to be_falsey
            end
          end

          context 'when job has no tag' do
            it 'picks build' do
              expect(build).to eq(pending_job)
            end

            context 'when runner has tag' do
              before do
                project_runner.update!(tag_list: ["win32"])
              end

              it 'picks build' do
                expect(build).to eq(pending_job)
              end
            end
          end
        end

        context 'deleted projects' do
          before do
            project.update!(pending_delete: true)
          end

          context 'for shared runners' do
            before do
              project.update!(shared_runners_enabled: true)
            end

            it 'does not pick a build' do
              expect(build_on(shared_runner)).to be_nil
            end
          end

          context 'for project runner' do
            subject(:build) { build_on(project_runner, runner_manager: project_runner_manager) }

            let(:project_runner_manager) { nil }

            context 'with no runner manager specified' do
              it 'does not pick a build' do
                expect(build).to be_nil
                expect(pending_job.reload).to be_failed
                expect(pending_job.queuing_entry).to be_nil
                expect(Ci::RunnerManagerBuild.all).to be_empty
              end
            end

            context 'with runner manager specified' do
              let(:project_runner_manager) { create(:ci_runner_machine, runner: project_runner) }

              it 'does not pick a build' do
                expect(build).to be_nil
                expect(pending_job.reload).to be_failed
                expect(pending_job.queuing_entry).to be_nil
                expect(Ci::RunnerManagerBuild.all).to be_empty
              end
            end
          end
        end

        context 'allow shared runners' do
          before do
            project.update!(shared_runners_enabled: true)
            pipeline.reload
            pending_job.reload
            pending_job.create_queuing_entry!
          end

          context 'when build owner has been blocked' do
            let(:user) { create(:user, :blocked) }

            before do
              pending_job.update!(user: user)
            end

            context 'with no runner manager specified' do
              it 'does not pick the build and drops the build' do
                expect(build_on(shared_runner)).to be_falsey

                expect(pending_job.reload).to be_user_blocked
              end
            end

            context 'with runner manager specified' do
              let(:runner_manager) { create(:ci_runner_machine, runner: runner) }

              it 'does not pick the build and does not create join record' do
                expect(build_on(shared_runner, runner_manager: runner_manager)).to be_falsey

                expect(Ci::RunnerManagerBuild.all).to be_empty
              end
            end
          end

          context 'for multiple builds' do
            let!(:project2) { create :project, shared_runners_enabled: true }
            let!(:pipeline2) { create :ci_pipeline, project: project2 }
            let!(:project3) { create :project, shared_runners_enabled: true }
            let!(:pipeline3) { create :ci_pipeline, project: project3 }
            let!(:build1_project1) { pending_job }
            let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
            let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
            let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
            let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
            let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }

            it 'picks builds one-by-one' do
              expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original

              expect(build_on(shared_runner)).to eq(build1_project1)
            end

            context 'when using fair scheduling' do
              context 'when all builds are pending' do
                it 'prefers projects without builds first' do
                  # it gets for one build from each of the projects
                  expect(build_on(shared_runner)).to eq(build1_project1)
                  expect(build_on(shared_runner)).to eq(build1_project2)
                  expect(build_on(shared_runner)).to eq(build1_project3)

                  # then it gets a second build from each of the projects
                  expect(build_on(shared_runner)).to eq(build2_project1)
                  expect(build_on(shared_runner)).to eq(build2_project2)

                  # in the end the third build
                  expect(build_on(shared_runner)).to eq(build3_project1)
                end
              end

              context 'when some builds transition to success' do
                it 'equalises number of running builds' do
                  # after finishing the first build for project 1, get a second build from the same project
                  expect(build_on(shared_runner)).to eq(build1_project1)
                  build1_project1.reload.success
                  expect(build_on(shared_runner)).to eq(build2_project1)

                  expect(build_on(shared_runner)).to eq(build1_project2)
                  build1_project2.reload.success
                  expect(build_on(shared_runner)).to eq(build2_project2)
                  expect(build_on(shared_runner)).to eq(build1_project3)
                  expect(build_on(shared_runner)).to eq(build3_project1)
                end
              end
            end

            context 'when using DEFCON mode that disables fair scheduling' do
              before do
                stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
              end

              context 'when all builds are pending' do
                it 'returns builds in order of creation (FIFO)' do
                  # it gets for one build from each of the projects
                  expect(build_on(shared_runner)).to eq(build1_project1)
                  expect(build_on(shared_runner)).to eq(build2_project1)
                  expect(build_on(shared_runner)).to eq(build3_project1)
                  expect(build_on(shared_runner)).to eq(build1_project2)
                  expect(build_on(shared_runner)).to eq(build2_project2)
                  expect(build_on(shared_runner)).to eq(build1_project3)
                end
              end

              context 'when some builds transition to success' do
                it 'returns builds in order of creation (FIFO)' do
                  expect(build_on(shared_runner)).to eq(build1_project1)
                  build1_project1.reload.success
                  expect(build_on(shared_runner)).to eq(build2_project1)

                  expect(build_on(shared_runner)).to eq(build3_project1)
                  build2_project1.reload.success
                  expect(build_on(shared_runner)).to eq(build1_project2)
                  expect(build_on(shared_runner)).to eq(build2_project2)
                  expect(build_on(shared_runner)).to eq(build1_project3)
                end
              end
            end
          end

          context 'shared runner' do
            let(:response) { described_class.new(shared_runner, nil).execute }
            let(:build) { response.build }

            it { expect(build).to be_kind_of(Build) }
            it { expect(build).to be_valid }
            it { expect(build).to be_running }
            it { expect(build.runner).to eq(shared_runner) }
            it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
          end

          context 'project runner' do
            let(:build) { build_on(project_runner) }

            it { expect(build).to be_kind_of(Build) }
            it { expect(build).to be_valid }
            it { expect(build).to be_running }
            it { expect(build.runner).to eq(project_runner) }
          end
        end

        context 'disallow shared runners' do
          before do
            project.update!(shared_runners_enabled: false)
          end

          context 'shared runner' do
            let(:build) { build_on(shared_runner) }

            it { expect(build).to be_nil }
          end

          context 'project runner' do
            let(:build) { build_on(project_runner) }

            it { expect(build).to be_kind_of(Build) }
            it { expect(build).to be_valid }
            it { expect(build).to be_running }
            it { expect(build.runner).to eq(project_runner) }
          end
        end

        context 'disallow when builds are disabled' do
          before do
            project.update!(shared_runners_enabled: true, group_runners_enabled: true)
            project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)

            pending_job.reload.create_queuing_entry!
          end

          context 'and uses shared runner' do
            let(:build) { build_on(shared_runner) }

            it { expect(build).to be_nil }
          end

          context 'and uses group runner' do
            let(:build) { build_on(group_runner) }

            it { expect(build).to be_nil }
          end

          context 'and uses project runner' do
            let(:build) { build_on(project_runner) }

            it 'does not pick a build' do
              expect(build).to be_nil
              expect(pending_job.reload).to be_failed
              expect(pending_job.queuing_entry).to be_nil
            end
          end
        end

        context 'allow group runners' do
          before do
            project.update!(group_runners_enabled: true)
          end

          context 'for multiple builds' do
            let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
            let!(:pipeline2) { create(:ci_pipeline, project: project2) }
            let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
            let!(:pipeline3) { create(:ci_pipeline, project: project3) }

            let!(:build1_project1) { pending_job }
            let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
            let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
            let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
            let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
            let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }

            # these shouldn't influence the scheduling
            let!(:unrelated_group) { create(:group) }
            let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
            let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
            let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
            let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }

            it 'does not consider builds from other group runners' do
              queue = ::Ci::Queue::BuildQueueService.new(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 6
              build_on(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 5
              build_on(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 4
              build_on(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 3
              build_on(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 2
              build_on(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 1
              build_on(group_runner)

              expect(queue.builds_for_group_runner.size).to eq 0
              expect(build_on(group_runner)).to be_nil
            end
          end

          context 'group runner' do
            let(:build) { build_on(group_runner) }

            it { expect(build).to be_kind_of(Build) }
            it { expect(build).to be_valid }
            it { expect(build).to be_running }
            it { expect(build.runner).to eq(group_runner) }
          end
        end

        context 'disallow group runners' do
          before do
            project.update!(group_runners_enabled: false)

            pending_job.reload.create_queuing_entry!
          end

          context 'group runner' do
            let(:build) { build_on(group_runner) }

            it { expect(build).to be_nil }
          end
        end

        context 'when first build is stalled' do
          before do
            allow_any_instance_of(described_class).to receive(:assign_runner!).and_call_original
            allow_any_instance_of(described_class).to receive(:assign_runner!)
              .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
          end

          subject { described_class.new(project_runner, nil).execute }

          context 'with multiple builds are in queue' do
            let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

            before do
              allow_any_instance_of(::Ci::Queue::BuildQueueService)
                .to receive(:execute)
                .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
            end

            it "receives second build from the queue" do
              expect(subject).to be_valid
              expect(subject.build).to eq(other_build)
            end
          end

          context 'when single build is in queue' do
            before do
              allow_any_instance_of(::Ci::Queue::BuildQueueService)
                .to receive(:execute)
                .and_return(Ci::Build.where(id: pending_job).pluck(:id))
            end

            it "does not receive any valid result" do
              expect(subject).not_to be_valid
            end
          end

          context 'when there is no build in queue' do
            before do
              allow_any_instance_of(::Ci::Queue::BuildQueueService)
                .to receive(:execute)
                .and_return([])
            end

            it "does not receive builds but result is valid" do
              expect(subject).to be_valid
              expect(subject.build).to be_nil
            end
          end
        end

        context 'when access_level of runner is not_protected' do
          let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }

          context 'when a job is protected' do
            let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }

            it 'picks the job' do
              expect(build_on(project_runner)).to eq(pending_job)
            end
          end

          context 'when a job is unprotected' do
            let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

            it 'picks the job' do
              expect(build_on(project_runner)).to eq(pending_job)
            end
          end

          context 'when protected attribute of a job is nil' do
            let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

            before do
              pending_job.update_attribute(:protected, nil)
            end

            it 'picks the job' do
              expect(build_on(project_runner)).to eq(pending_job)
            end
          end
        end

        context 'when access_level of runner is ref_protected' do
          let!(:project_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }

          context 'when a job is protected' do
            let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }

            it 'picks the job' do
              expect(build_on(project_runner)).to eq(pending_job)
            end
          end

          context 'when a job is unprotected' do
            let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

            it 'does not pick the job' do
              expect(build_on(project_runner)).to be_nil
            end
          end

          context 'when protected attribute of a job is nil' do
            let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

            before do
              pending_job.update_attribute(:protected, nil)
            end

            it 'does not pick the job' do
              expect(build_on(project_runner)).to be_nil
            end
          end
        end

        context 'runner feature set is verified' do
          let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
          let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }

          subject { build_on(project_runner, params: params) }

          context 'when feature is missing by runner' do
            let(:params) { {} }

            it 'does not pick the build and drops the build' do
              expect(subject).to be_nil
              expect(pending_job.reload).to be_failed
              expect(pending_job).to be_runner_unsupported
            end
          end

          context 'when feature is supported by runner' do
            let(:params) do
              { info: { features: { upload_multiple_artifacts: true } } }
            end

            it 'does pick job' do
              expect(subject).not_to be_nil
            end
          end
        end

        context 'when "dependencies" keyword is specified' do
          let!(:pre_stage_job) do
            create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
          end

          let!(:pending_job) do
            create(:ci_build, :pending, :queued,
              pipeline: pipeline, stage_idx: 1,
              options: { script: ["bash"], dependencies: dependencies })
          end

          let(:dependencies) { %w[test] }

          subject { build_on(project_runner) }

          it 'picks a build with a dependency' do
            picked_build = build_on(project_runner)

            expect(picked_build).to be_present
          end

          context 'when there are multiple dependencies with artifacts' do
            let!(:pre_stage_job_second) do
              create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
            end

            let(:dependencies) { %w[test deploy] }

            it 'logs build artifacts size' do
              build_on(project_runner)

              artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
                job.job_artifacts_archive.size
              end

              expect(artifacts_size).to eq 107464 * 2
              expect(Gitlab::ApplicationContext.current).to include({
                'meta.artifacts_dependencies_size' => artifacts_size,
                'meta.artifacts_dependencies_count' => 2
              })
            end
          end

          shared_examples 'not pick' do
            it 'does not pick the build and drops the build' do
              expect(subject).to be_nil
              expect(pending_job.reload).to be_failed
              expect(pending_job).to be_missing_dependency_failure
            end
          end

          shared_examples 'validation is active' do
            context 'when depended job has not been completed yet' do
              let!(:pre_stage_job) do
                create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
              end

              it { is_expected.to eq(pending_job) }
            end

            context 'when artifacts of depended job has been expired' do
              let!(:pre_stage_job) do
                create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
              end

              context 'when the pipeline is locked' do
                before do
                  pipeline.artifacts_locked!
                end

                it { is_expected.to eq(pending_job) }
              end

              context 'when the pipeline is unlocked' do
                before do
                  pipeline.unlocked!
                end

                it_behaves_like 'not pick'
              end
            end

            context 'when artifacts of depended job has been erased' do
              let!(:pre_stage_job) do
                create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
              end

              it_behaves_like 'not pick'
            end

            context 'when job object is staled' do
              let!(:pre_stage_job) do
                create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
              end

              before do
                pipeline.unlocked!

                allow_next_instance_of(Ci::Build) do |build|
                  expect(build).to receive(:drop!)
                    .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
                end
              end

              it 'does not drop nor pick' do
                expect(subject).to be_nil
              end
            end
          end

          shared_examples 'validation is not active' do
            context 'when depended job has not been completed yet' do
              let!(:pre_stage_job) do
                create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
              end

              it { expect(subject).to eq(pending_job) }
            end

            context 'when artifacts of depended job has been expired' do
              let!(:pre_stage_job) do
                create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
              end

              it { expect(subject).to eq(pending_job) }
            end

            context 'when artifacts of depended job has been erased' do
              let!(:pre_stage_job) do
                create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
              end

              it { expect(subject).to eq(pending_job) }
            end
          end

          it_behaves_like 'validation is active'
        end

        context 'when build is degenerated' do
          let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }

          subject { build_on(project_runner) }

          it 'does not pick the build and drops the build' do
            expect(subject).to be_nil

            pending_job.reload
            expect(pending_job).to be_failed
            expect(pending_job).to be_archived_failure
          end
        end

        context 'when build has data integrity problem' do
          let!(:pending_job) do
            create(:ci_build, :pending, :queued, pipeline: pipeline)
          end

          before do
            pending_job.update_columns(options: "string")
          end

          subject { build_on(project_runner) }

          it 'does drop the build and logs both failures' do
            expect(Gitlab::ErrorTracking).to receive(:track_exception)
              .with(anything, a_hash_including(build_id: pending_job.id))
              .twice
              .and_call_original

            expect(subject).to be_nil

            pending_job.reload
            expect(pending_job).to be_failed
            expect(pending_job).to be_data_integrity_failure
          end
        end

        context 'when build fails to be run!' do
          let!(:pending_job) do
            create(:ci_build, :pending, :queued, pipeline: pipeline)
          end

          before do
            expect_any_instance_of(Ci::Build).to receive(:run!)
              .and_raise(RuntimeError, 'scheduler error')
          end

          subject { build_on(project_runner) }

          it 'does drop the build and logs failure' do
            expect(Gitlab::ErrorTracking).to receive(:track_exception)
              .with(anything, a_hash_including(build_id: pending_job.id))
              .once
              .and_call_original

            expect(subject).to be_nil

            pending_job.reload
            expect(pending_job).to be_failed
            expect(pending_job).to be_scheduler_failure
          end
        end

        context 'when an exception is raised during a persistent ref creation' do
          before do
            allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
            allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
          end

          subject { build_on(project_runner) }

          it 'picks the build' do
            expect(subject).to eq(pending_job)

            pending_job.reload
            expect(pending_job).to be_running
          end
        end

        context 'when only some builds can be matched by runner' do
          let!(:project_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
          let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }

          before do
            # create additional matching and non-matching jobs
            create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
            create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
          end

          it 'observes queue size of only matching jobs' do
            # pending_job + 2 x matching ones
            expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
              .with({ runner_type: project_runner.runner_type }, 3)

            expect(build_on(project_runner)).to eq(pending_job)
          end

          it 'observes queue processing time by the runner type' do
            expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
              .to receive(:observe)
              .with({ runner_type: project_runner.runner_type }, anything)

            expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
              .to receive(:observe)
              .with({ runner_type: project_runner.runner_type }, anything)

            expect(build_on(project_runner)).to eq(pending_job)
          end
        end

        context 'when ci_register_job_temporary_lock is enabled' do
          before do
            stub_feature_flags(ci_register_job_temporary_lock: true)

            allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
          end

          context 'when a build is temporarily locked' do
            let(:service) { described_class.new(project_runner, nil) }

            before do
              service.send(:acquire_temporary_lock, pending_job.id)
            end

            it 'skips this build and marks queue as invalid' do
              expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
                .with(operation: :queue_iteration)
              expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
                .with(operation: :build_temporary_locked)

              expect(service.execute).not_to be_valid
            end

            context 'when there is another build in queue' do
              let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

              it 'skips this build and picks another build' do
                expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
                  .with(operation: :queue_iteration).twice
                expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
                  .with(operation: :build_temporary_locked)

                result = service.execute

                expect(result.build).to eq(next_pending_job)
                expect(result).to be_valid
              end
            end
          end
        end
      end

      context 'when using pending builds table' do
        let!(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }

        include_examples 'handles runner assignment'

        context 'when a conflicting data is stored in denormalized table' do
          let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }

          before do
            pending_job.update_column(:status, :running)
          end

          it 'removes queuing entry upon build assignment attempt' do
            expect(pending_job.reload).to be_running
            expect(pending_job.queuing_entry).to be_present

            expect(execute).not_to be_valid
            expect(pending_job.reload.queuing_entry).not_to be_present
          end
        end
      end
    end

    describe '#register_success' do
      let!(:current_time) { Time.zone.local(2018, 4, 5, 14, 0, 0) }
      let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
      let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }

      before do
        allow(Time).to receive(:now).and_return(current_time)
        # Stub tested metrics
        allow(Gitlab::Ci::Queue::Metrics)
          .to receive(:attempt_counter)
                .and_return(attempt_counter)

        allow(Gitlab::Ci::Queue::Metrics)
          .to receive(:job_queue_duration_seconds)
                .and_return(job_queue_duration_seconds)

        project.update!(shared_runners_enabled: true)
        pending_job.update!(created_at: current_time - 3600, queued_at: current_time - 1800)
      end

      shared_examples 'attempt counter collector' do
        it 'increments attempt counter' do
          allow(job_queue_duration_seconds).to receive(:observe)
          expect(attempt_counter).to receive(:increment)

          build_on(runner)
        end
      end

      shared_examples 'jobs queueing time histogram collector' do
        it 'counts job queuing time histogram with expected labels' do
          allow(attempt_counter).to receive(:increment)
          expect(job_queue_duration_seconds).to receive(:observe)
            .with({ shared_runner: expected_shared_runner,
                    jobs_running_for_project: expected_jobs_running_for_project_first_job,
                    shard: expected_shard }, 1800)

          build_on(runner)
        end

        context 'when project already has running jobs' do
          let(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
          let(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }

          before do
            ::Ci::RunningBuild.upsert_shared_runner_build!(build2)
            ::Ci::RunningBuild.upsert_shared_runner_build!(build3)
          end

          it 'counts job queuing time histogram with expected labels' do
            allow(attempt_counter).to receive(:increment)

            expect(job_queue_duration_seconds).to receive(:observe)
              .with({ shared_runner: expected_shared_runner,
                      jobs_running_for_project: expected_jobs_running_for_project_third_job,
                      shard: expected_shard }, 1800)

            build_on(runner)
          end
        end
      end

      shared_examples 'metrics collector' do
        it_behaves_like 'attempt counter collector'
        it_behaves_like 'jobs queueing time histogram collector'
      end

      context 'when shared runner is used' do
        before do
          pending_job.reload
          pending_job.create_queuing_entry!
        end

        let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 tag2]) }
        let(:expected_shared_runner) { true }
        let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
        let(:expected_jobs_running_for_project_first_job) { '0' }
        let(:expected_jobs_running_for_project_third_job) { '2' }

        it_behaves_like 'metrics collector'

        context 'when metrics_shard tag is defined' do
          let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 metrics_shard::shard_tag tag2]) }
          let(:expected_shard) { 'shard_tag' }

          it_behaves_like 'metrics collector'
        end

        context 'when multiple metrics_shard tag is defined' do
          let(:runner) { create(:ci_runner, :instance, tag_list: %w[tag1 metrics_shard::shard_tag metrics_shard::shard_tag_2 tag2]) }
          let(:expected_shard) { 'shard_tag' }

          it_behaves_like 'metrics collector'
        end

        context 'when max running jobs bucket size is exceeded' do
          before do
            stub_const('Project::INSTANCE_RUNNER_RUNNING_JOBS_MAX_BUCKET', 1)
          end

          let(:expected_jobs_running_for_project_third_job) { '1+' }

          it_behaves_like 'metrics collector'
        end

        context 'when pending job with queued_at=nil is used' do
          before do
            pending_job.update!(queued_at: nil)
          end

          it_behaves_like 'attempt counter collector'

          it "doesn't count job queuing time histogram" do
            allow(attempt_counter).to receive(:increment)
            expect(job_queue_duration_seconds).not_to receive(:observe)

            build_on(runner)
          end
        end
      end

      context 'when project runner is used' do
        let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[tag1 metrics_shard::shard_tag tag2]) }
        let(:expected_shared_runner) { false }
        let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
        let(:expected_jobs_running_for_project_first_job) { '+Inf' }
        let(:expected_jobs_running_for_project_third_job) { '+Inf' }

        it_behaves_like 'metrics collector'
      end
    end

    context 'when runner_session params are' do
      it 'present sets runner session configuration in the build' do
        runner_session_params = { session: { 'url' => 'https://example.com' } }

        expect(build_on(project_runner, params: runner_session_params).runner_session.attributes)
          .to include(runner_session_params[:session])
      end

      it 'not present it does not configure the runner session' do
        expect(build_on(project_runner).runner_session).to be_nil
      end
    end

    context 'when max queue depth is reached' do
      let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
      let!(:pending_job_2) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
      let!(:pending_job_3) { create(:ci_build, :pending, :queued, pipeline: pipeline) }

      before do
        stub_const("#{described_class}::MAX_QUEUE_DEPTH", 2)
      end

      it 'returns 409 conflict' do
        expect(Ci::Build.pending.unstarted.count).to eq 3

        result = described_class.new(project_runner, nil).execute

        expect(result).not_to be_valid
        expect(result.build).to be_nil
        expect(result.build_json).to be_nil
      end
    end

    def build_on(runner, runner_manager: nil, params: {})
      described_class.new(runner, runner_manager).execute(params).build
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class ParseAnnotationsArtifactService < ::BaseService
    include ::Gitlab::Utils::StrongMemoize
    include ::Gitlab::EncodingHelper

    SizeLimitError = Class.new(StandardError)
    ParserError = Class.new(StandardError)

    def execute(artifact)
      return error('Artifact is not annotations file type', :bad_request) unless artifact&.annotations?

      return error("Annotations Artifact Too Big. Maximum Allowable Size: #{annotations_size_limit}", :bad_request) if
        artifact.file.size > annotations_size_limit

      annotations = parse!(artifact)
      Ci::JobAnnotation.bulk_upsert!(annotations, unique_by: %i[partition_id job_id name])

      success
    rescue SizeLimitError, ParserError, Gitlab::Json.parser_error, ActiveRecord::RecordInvalid => error
      error(error.message, :bad_request)
    end

    private

    def parse!(artifact)
      annotations = []

      artifact.each_blob do |blob|
        # Windows powershell may output UTF-16LE files, so convert the whole file
        # to UTF-8 before proceeding.
        blob = strip_bom(encode_utf8_with_replacement_character(blob))

        blob_json = Gitlab::Json.parse(blob)
        raise ParserError, 'Annotations files must be a JSON object' unless blob_json.is_a?(Hash)

        blob_json.each do |key, value|
          annotations.push(Ci::JobAnnotation.new(job: artifact.job, name: key, data: value))

          if annotations.size > annotations_num_limit
            raise SizeLimitError,
              "Annotations files cannot have more than #{annotations_num_limit} annotation lists"
          end
        end
      end

      annotations
    end

    def annotations_num_limit
      project.actual_limits.ci_job_annotations_num
    end
    strong_memoize_attr :annotations_num_limit

    def annotations_size_limit
      project.actual_limits.ci_job_annotations_size
    end
    strong_memoize_attr :annotations_size_limit
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ParseAnnotationsArtifactService, feature_category: :build_artifacts do
  let_it_be(:project) { create(:project) }

  let_it_be_with_reload(:build) { create(:ci_build, project: project) }
  let(:service) { described_class.new(project, nil) }

  describe '#execute' do
    subject { service.execute(artifact) }

    context 'when build has an annotations artifact' do
      let_it_be(:artifact) { create(:ci_job_artifact, :annotations, job: build) }

      context 'when artifact does not have the specified blob' do
        before do
          allow(artifact).to receive(:each_blob)
        end

        it 'parses nothing' do
          expect(subject[:status]).to eq(:success)

          expect(build.job_annotations).to be_empty
        end
      end

      context 'when artifact has the specified blob' do
        let(:blob) { data.to_json }

        before do
          allow(artifact).to receive(:each_blob).and_yield(blob)
        end

        context 'when valid annotations are given' do
          let(:data) do
            {
              external_links: [
                {
                  external_link: {
                    label: 'URL 1',
                    url: 'https://url1.example.com/'
                  }
                },
                {
                  external_link: {
                    label: 'URL 2',
                    url: 'https://url2.example.com/'
                  }
                }
              ]
            }
          end

          it 'parses the artifact' do
            subject

            expect(build.job_annotations.as_json).to contain_exactly(
              hash_including('name' => 'external_links', 'data' => [
                hash_including('external_link' => hash_including('label' => 'URL 1', 'url' => 'https://url1.example.com/')),
                hash_including('external_link' => hash_including('label' => 'URL 2', 'url' => 'https://url2.example.com/'))
              ])
            )
          end
        end

        context 'when valid annotations are given and annotation list name is the same' do
          before do
            build.job_annotations.create!(name: 'external_links', data: [
              {
                external_link: {
                  label: 'URL 1',
                  url: 'https://url1.example.com/'
                }
              }
            ])
          end

          let(:data) do
            {
              external_links: [
                {
                  external_link: {
                    label: 'URL 2',
                    url: 'https://url2.example.com/'
                  }
                }
              ]
            }
          end

          it 'parses the artifact' do
            subject

            expect(build.job_annotations.as_json).to contain_exactly(
              hash_including('name' => 'external_links', 'data' => [
                hash_including('external_link' => hash_including('label' => 'URL 2', 'url' => 'https://url2.example.com/'))
              ])
            )
          end
        end

        context 'when invalid JSON is given' do
          let(:blob) { 'Invalid JSON!' }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when root is not an object' do
          let(:data) { [] }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq('Annotations files must be a JSON object')
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when item is not a valid annotation list' do
          let(:data) { { external_links: {} } }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq('Validation failed: Data must be a valid json schema')
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when more than limitated annotations are specified in annotations' do
          let(:data) do
            {
              external_links_1: [
                {
                  external_link: {
                    label: 'URL',
                    url: 'https://example.com/'
                  }
                }
              ],
              external_links_2: [
                {
                  external_link: {
                    label: 'URL',
                    url: 'https://example.com/'
                  }
                }
              ]
            }
          end

          before do
            allow(service).to receive(:annotations_num_limit).and_return(1)
          end

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq(
              "Annotations files cannot have more than #{service.send(:annotations_num_limit)} annotation lists")
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end
      end

      context 'when artifact size is too big' do
        before do
          allow(artifact.file).to receive(:size) { service.send(:annotations_size_limit) + 1.kilobyte }
        end

        it 'returns error' do
          expect(subject[:status]).to eq(:error)
          expect(subject[:message]).to eq(
            "Annotations Artifact Too Big. Maximum Allowable Size: #{service.send(:annotations_size_limit)}")
          expect(subject[:http_status]).to eq(:bad_request)
        end
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class ParseDotenvArtifactService < ::BaseService
    include ::Gitlab::Utils::StrongMemoize
    include ::Gitlab::EncodingHelper

    SizeLimitError = Class.new(StandardError)
    ParserError = Class.new(StandardError)

    def execute(artifact)
      validate!(artifact)

      variables = parse!(artifact)
      Ci::JobVariable.bulk_insert!(variables)

      success
    rescue SizeLimitError, ParserError, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => error
      Gitlab::ErrorTracking.track_exception(error, job_id: artifact.job_id)
      error(error.message, :bad_request)
    end

    private

    def validate!(artifact)
      unless artifact&.dotenv?
        raise ArgumentError, 'Artifact is not dotenv file type'
      end

      unless artifact.file.size < dotenv_size_limit
        raise SizeLimitError,
          "Dotenv Artifact Too Big. Maximum Allowable Size: #{dotenv_size_limit}"
      end
    end

    def parse!(artifact)
      variables = {}

      artifact.each_blob do |blob|
        # Windows powershell may output UTF-16LE files, so convert the whole file
        # to UTF-8 before proceeding.
        blob = strip_bom(encode_utf8_with_replacement_character(blob))

        blob.each_line do |line|
          key, value = scan_line!(line)

          variables[key] = Ci::JobVariable.new(
            job_id: artifact.job_id,
            source: :dotenv,
            key: key,
            value: value,
            raw: false
          )
        end
      end

      if variables.size > dotenv_variable_limit
        raise SizeLimitError,
          "Dotenv files cannot have more than #{dotenv_variable_limit} variables"
      end

      variables.values
    end

    def scan_line!(line)
      result = line.scan(/^(.*?)=(.*)$/).last

      raise ParserError, 'Invalid Format' if result.nil?

      result.each(&:strip!)
    end

    def dotenv_variable_limit
      strong_memoize(:dotenv_variable_limit) { project.actual_limits.dotenv_variables }
    end

    def dotenv_size_limit
      strong_memoize(:dotenv_size_limit) { project.actual_limits.dotenv_size }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ParseDotenvArtifactService, feature_category: :build_artifacts do
  let_it_be(:project) { create(:project) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  let(:build) { create(:ci_build, pipeline: pipeline, project: project) }
  let(:service) { described_class.new(project, nil) }

  describe '#execute' do
    subject { service.execute(artifact) }

    context 'when build has a dotenv artifact' do
      let!(:artifact) { create(:ci_job_artifact, :dotenv, job: build) }

      it 'parses the artifact' do
        expect(subject[:status]).to eq(:success)

        expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
          hash_including('key' => 'KEY1', 'value' => 'VAR1'),
          hash_including('key' => 'KEY2', 'value' => 'VAR2'))
      end

      context 'when dotenv variables are conflicting against manual variables' do
        before do
          create(:ci_job_variable, job: build, key: 'KEY1')
        end

        it 'returns an error message that there is a duplicate variable' do
          subject

          expect(subject[:status]).to eq(:error)
          expect(subject[:message]).to include("Key (key, job_id)=(KEY1, #{build.id}) already exists.")
          expect(subject[:http_status]).to eq(:bad_request)
        end
      end

      context 'when dotenv variables have duplicate variables' do
        let!(:artifact) { create(:ci_job_artifact, :dotenv, job: build) }
        let(:blob) do
          <<~EOS
            KEY1=VAR1
            KEY2=VAR2
            KEY2=VAR3
            KEY1=VAR4
          EOS
        end

        before do
          allow(artifact).to receive(:each_blob).and_yield(blob)
        end

        it 'latest values get used' do
          subject

          expect(subject[:status]).to eq(:success)

          expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
            hash_including('key' => 'KEY1', 'value' => 'VAR4'),
            hash_including('key' => 'KEY2', 'value' => 'VAR3'))
        end
      end

      context 'when parse error happens' do
        before do
          allow(service).to receive(:scan_line!) { raise described_class::ParserError, 'Invalid Format' }
        end

        it 'returns error' do
          expect(Gitlab::ErrorTracking).to receive(:track_exception)
            .with(described_class::ParserError, job_id: build.id)

          expect(subject[:status]).to eq(:error)
          expect(subject[:message]).to eq('Invalid Format')
          expect(subject[:http_status]).to eq(:bad_request)
        end
      end

      context 'when artifact size is too big' do
        before do
          allow(artifact.file).to receive(:size) { 10.kilobytes }
        end

        it 'returns error' do
          expect(subject[:status]).to eq(:error)
          expect(subject[:message]).to eq("Dotenv Artifact Too Big. Maximum Allowable Size: #{service.send(:dotenv_size_limit)}")
          expect(subject[:http_status]).to eq(:bad_request)
        end
      end

      context 'when artifact has the specified blob' do
        before do
          allow(artifact).to receive(:each_blob).and_yield(blob)
        end

        context 'when a white space trails the key' do
          let(:blob) { 'KEY1 =VAR1' }

          it 'trims the trailing space' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => 'VAR1'))
          end
        end

        context 'when multiple key/value pairs exist in one line' do
          let(:blob) { 'KEY=VARCONTAINING=EQLS' }

          it 'parses the dotenv data' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS'))
          end
        end

        context 'when key contains UNICODE' do
          let(:blob) { '🛹=skateboard' }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.")
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when value contains UNICODE' do
          let(:blob) { 'skateboard=🛹' }

          it 'parses the dotenv data' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'skateboard', 'value' => '🛹'))
          end
        end

        context 'when key contains a space' do
          let(:blob) { 'K E Y 1=VAR1' }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.")
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when value contains a space' do
          let(:blob) { 'KEY1=V A R 1' }

          it 'parses the dotenv data' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => 'V A R 1'))
          end
        end

        context 'when value is double quoated' do
          let(:blob) { 'KEY1="VAR1"' }

          it 'parses the value as-is' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => '"VAR1"'))
          end
        end

        context 'when value is single quoated' do
          let(:blob) { "KEY1='VAR1'" }

          it 'parses the value as-is' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => "'VAR1'"))
          end
        end

        context 'when value has white spaces in double quote' do
          let(:blob) { 'KEY1="  VAR1  "' }

          it 'parses the value as-is' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => '"  VAR1  "'))
          end
        end

        context 'when key is missing' do
          let(:blob) { '=VAR1' }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to match(/Key can't be blank/)
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when value is missing' do
          let(:blob) { 'KEY1=' }

          it 'parses the dotenv data' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => ''))
          end
        end

        context 'when it is not dotenv format' do
          let(:blob) { "{ 'KEY1': 'VAR1' }" }

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq('Invalid Format')
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when blob is encoded in UTF-16 LE' do
          let(:blob) { File.read(Rails.root.join('spec/fixtures/build_artifacts/dotenv_utf16_le.txt')) }

          it 'parses the dotenv data' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'MY_ENV_VAR', 'value' => 'true'),
              hash_including('key' => 'TEST2', 'value' => 'false'))
          end
        end

        context 'when more than limitated variables are specified in dotenv' do
          let(:blob) do
            StringIO.new.tap do |s|
              (service.send(:dotenv_variable_limit) + 1).times do |i|
                s << "KEY#{i}=VAR#{i}\n"
              end
            end.string
          end

          it 'returns error' do
            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq("Dotenv files cannot have more than #{service.send(:dotenv_variable_limit)} variables")
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when variables are cross-referenced in dotenv' do
          let(:blob) do
            <<~EOS
              KEY1=VAR1
              KEY2=${KEY1}_Test
            EOS
          end

          it 'does not support variable expansion in dotenv parser' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => 'VAR1'),
              hash_including('key' => 'KEY2', 'value' => '${KEY1}_Test'))
          end
        end

        context 'when there is an empty line' do
          let(:blob) do
            <<~EOS
              KEY1=VAR1

              KEY2=VAR2
            EOS
          end

          it 'does not support empty line in dotenv parser' do
            subject

            expect(subject[:status]).to eq(:error)
            expect(subject[:message]).to eq('Invalid Format')
            expect(subject[:http_status]).to eq(:bad_request)
          end
        end

        context 'when there is a comment' do
          let(:blob) do
            <<~EOS
              KEY1=VAR1         # This is variable
            EOS
          end

          it 'does not support comment in dotenv parser' do
            subject

            expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
              hash_including('key' => 'KEY1', 'value' => 'VAR1         # This is variable'))
          end
        end
      end
    end

    context 'when build does not have a dotenv artifact' do
      let!(:artifact) {}

      it 'raises an error' do
        expect { subject }.to raise_error(ArgumentError)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  # TODO: a couple of points with this approach:
  # + reuses existing architecture and reactive caching
  # - it's not a report comparison and some comparing features must be turned off.
  # see CompareReportsBaseService for more notes.
  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
  class GenerateTerraformReportsService < CompareReportsBaseService
    def execute(base_pipeline, head_pipeline)
      {
        status: :parsed,
        key: key(base_pipeline, head_pipeline),
        data: head_pipeline.terraform_reports.plans
      }
    rescue StandardError => e
      Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
      {
        status: :error,
        key: key(base_pipeline, head_pipeline),
        status_reason: _('An error occurred while fetching terraform reports.')
      }
    end

    def latest?(base_pipeline, head_pipeline, data)
      data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::GenerateTerraformReportsService, feature_category: :infrastructure_as_code do
  let_it_be(:project) { create(:project, :repository) }

  describe '#execute' do
    let_it_be(:merge_request) { create(:merge_request, :with_terraform_reports, source_project: project) }

    subject { described_class.new(project, nil, id: merge_request.id) }

    context 'when head pipeline has terraform reports' do
      it 'returns status and data' do
        pipeline = merge_request.head_pipeline
        result = subject.execute(nil, pipeline)

        pipeline.builds.each do |build|
          expect(result).to match(
            status: :parsed,
            data: match(
              a_hash_including(build.id.to_s => hash_including(
                'create' => 0,
                'delete' => 0,
                'update' => 1,
                'job_name' => build.name
              ))
            ),
            key: an_instance_of(Array)
          )
        end
      end
    end

    context 'when head pipeline has corrupted terraform reports' do
      it 'returns a report with error messages' do
        build = create(:ci_build, pipeline: merge_request.head_pipeline, project: project)
        create(:ci_job_artifact, :terraform_with_corrupted_data, job: build, project: project)

        result = subject.execute(nil, merge_request.head_pipeline)

        expect(result).to match(
          status: :parsed,
          data: match(
            a_hash_including(build.id.to_s => hash_including(
              'tf_report_error' => :invalid_json_format
            ))
          ),
          key: an_instance_of(Array)
        )
      end
    end

    context 'when head pipeline is corrupted' do
      it 'returns status and error message' do
        result = subject.execute(nil, nil)

        expect(result).to match(
          a_hash_including(
            status: :error,
            status_reason: 'An error occurred while fetching terraform reports.'
          )
        )
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class AbortPipelinesService
    # NOTE: This call fails pipelines in bulk without running callbacks.
    # Only for pipeline abandonment scenarios (examples: project delete)
    def execute(pipelines, failure_reason)
      pipelines.cancelable.each_batch(of: 100) do |pipeline_batch|
        now = Time.current

        basic_attributes = { status: :failed }
        all_attributes = basic_attributes.merge(failure_reason: failure_reason, finished_at: now)

        bulk_fail_for(Ci::Stage, pipeline_batch, basic_attributes)
        bulk_fail_for(CommitStatus, pipeline_batch, all_attributes)

        pipeline_batch.update_all(all_attributes)
      end

      ServiceResponse.success(message: 'Pipelines stopped')
    end

    private

    def bulk_fail_for(klass, pipelines, attributes)
      klass.in_pipelines(pipelines)
        .cancelable
        .in_batches(of: 150) # rubocop:disable Cop/InBatches
        .update_all(attributes)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::AbortPipelinesService, feature_category: :continuous_integration do
  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project, namespace: user.namespace) }

  let_it_be(:cancelable_pipeline, reload: true) { create(:ci_pipeline, :running, project: project, user: user) }
  let_it_be(:manual_pipeline, reload: true) { create(:ci_pipeline, status: :manual, project: project, user: user) }
  let_it_be(:other_users_pipeline, reload: true) { create(:ci_pipeline, :running, project: project, user: create(:user)) } # not this user's pipeline

  let_it_be(:cancelable_build, reload: true) { create(:ci_build, :running, pipeline: cancelable_pipeline) }
  let_it_be(:non_cancelable_build, reload: true) { create(:ci_build, :success, pipeline: cancelable_pipeline) }
  let_it_be(:cancelable_stage, reload: true) { create(:ci_stage, name: 'stageA', status: :running, pipeline: cancelable_pipeline, project: project) }
  let_it_be(:non_cancelable_stage, reload: true) { create(:ci_stage, name: 'stageB', status: :success, pipeline: cancelable_pipeline, project: project) }

  let_it_be(:manual_pipeline_cancelable_build, reload: true) { create(:ci_build, :created, pipeline: manual_pipeline) }
  let_it_be(:manual_pipeline_non_cancelable_build, reload: true) { create(:ci_build, :manual, pipeline: manual_pipeline) }
  let_it_be(:manual_pipeline_cancelable_stage, reload: true) { create(:ci_stage, name: 'stageA', status: :created, pipeline: manual_pipeline, project: project) }
  let_it_be(:manual_pipeline_non_cancelable_stage, reload: true) { create(:ci_stage, name: 'stageB', status: :success, pipeline: manual_pipeline, project: project) }

  describe '#execute' do
    def expect_correct_pipeline_cancellations
      expect(cancelable_pipeline.finished_at).not_to be_nil
      expect(cancelable_pipeline).to be_failed

      expect(manual_pipeline.finished_at).not_to be_nil
      expect(manual_pipeline).to be_failed
    end

    def expect_correct_stage_cancellations
      expect(cancelable_pipeline.stages - [non_cancelable_stage]).to all(be_failed)
      expect(manual_pipeline.stages - [manual_pipeline_non_cancelable_stage]).to all(be_failed)

      expect(non_cancelable_stage).not_to be_failed
      expect(manual_pipeline_non_cancelable_stage).not_to be_failed
    end

    def expect_correct_build_cancellations
      expect(cancelable_build).to be_failed
      expect(cancelable_build.finished_at).not_to be_nil

      expect(manual_pipeline_cancelable_build).to be_failed
      expect(manual_pipeline_cancelable_build.finished_at).not_to be_nil

      expect(non_cancelable_build).not_to be_failed
      expect(manual_pipeline_non_cancelable_build).not_to be_failed
    end

    def expect_correct_cancellations
      expect_correct_pipeline_cancellations
      expect_correct_stage_cancellations
      expect_correct_build_cancellations
    end

    context 'with project pipelines' do
      def abort_project_pipelines
        described_class.new.execute(project.all_pipelines, :project_deleted)
      end

      it 'fails all running pipelines and related jobs' do
        expect(abort_project_pipelines).to be_success

        expect_correct_cancellations

        expect(other_users_pipeline.status).to eq('failed')
        expect(other_users_pipeline.failure_reason).to eq('project_deleted')
        expect(other_users_pipeline.stages.map(&:status)).to all(eq('failed'))
      end

      it 'avoids N+1 queries' do
        control_count = ActiveRecord::QueryRecorder.new { abort_project_pipelines }.count

        pipelines = create_list(:ci_pipeline, 5, :running, project: project)
        create_list(:ci_build, 5, :running, pipeline: pipelines.first)

        expect { abort_project_pipelines }.not_to exceed_query_limit(control_count)
      end

      context 'with live build logs' do
        before do
          create(:ci_build_trace_chunk, build: cancelable_build)
        end

        it 'makes failed builds with stale trace visible' do
          expect(Ci::Build.with_stale_live_trace.count).to eq 0

          travel_to(2.days.ago) do
            abort_project_pipelines
          end

          expect(Ci::Build.with_stale_live_trace.count).to eq 1
        end
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  class CompareAccessibilityReportsService < CompareReportsBaseService
    def comparer_class
      Gitlab::Ci::Reports::AccessibilityReportsComparer
    end

    def serializer_class
      AccessibilityReportsComparerSerializer
    end

    def get_report(pipeline)
      pipeline&.accessibility_reports
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::CompareAccessibilityReportsService, feature_category: :continuous_integration do
  let(:service) { described_class.new(project) }
  let(:project) { create(:project, :repository) }

  describe '#execute' do
    subject { service.execute(base_pipeline, head_pipeline) }

    context 'when head pipeline has accessibility reports' do
      let(:base_pipeline) { nil }
      let(:head_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }

      it 'returns status and data' do
        expect(subject[:status]).to eq(:parsed)
        expect(subject[:data]).to match_schema('entities/accessibility_reports_comparer')
      end
    end

    context 'when base and head pipelines have accessibility reports' do
      let(:base_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }
      let(:head_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }

      it 'returns status and data' do
        expect(subject[:status]).to eq(:parsed)
        expect(subject[:data]).to match_schema('entities/accessibility_reports_comparer')
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  module JobTokenScope
    class AddProjectService < ::BaseService
      include EditScopeValidations

      def execute(target_project, direction: :inbound)
        validate_edit!(project, target_project, current_user)

        link = allowlist(direction)
          .add!(target_project, user: current_user)

        ServiceResponse.success(payload: { project_link: link })

      rescue ActiveRecord::RecordNotUnique
        ServiceResponse.error(message: "Target project is already in the job token scope")
      rescue ActiveRecord::RecordInvalid => e
        ServiceResponse.error(message: e.message)
      rescue EditScopeValidations::ValidationError => e
        ServiceResponse.error(message: e.message)
      end

      private

      def allowlist(direction)
        Ci::JobToken::Allowlist.new(project, direction: direction)
      end
    end
  end
end

Ci::JobTokenScope::AddProjectService.prepend_mod_with('Ci::JobTokenScope::AddProjectService')

```

Response:
# frozen_string_literal: true
require 'spec_helper'

RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuous_integration do
  let(:service) { described_class.new(project, current_user) }

  let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
  let_it_be(:target_project) { create(:project) }
  let_it_be(:current_user) { create(:user) }

  shared_examples 'adds project' do |context|
    it 'adds the project to the scope' do
      expect do
        expect(result).to be_success
      end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
    end
  end

  describe '#execute' do
    subject(:result) { service.execute(target_project) }

    it_behaves_like 'editable job token scope' do
      context 'when user has permissions on source and target projects' do
        let(:resulting_direction) { result.payload.fetch(:project_link)&.direction }

        before do
          project.add_maintainer(current_user)
          target_project.add_developer(current_user)
        end

        it_behaves_like 'adds project'

        context 'when token scope is disabled' do
          before do
            project.ci_cd_settings.update!(job_token_scope_enabled: false)
          end

          it_behaves_like 'adds project'

          it 'creates an inbound link by default' do
            expect(resulting_direction).to eq('inbound')
          end

          context 'when direction is specified' do
            subject(:result) { service.execute(target_project, direction: direction) }

            context 'when the direction is outbound' do
              let(:direction) { :outbound }

              specify { expect(resulting_direction).to eq('outbound') }
            end

            context 'when the direction is inbound' do
              let(:direction) { :inbound }

              specify { expect(resulting_direction).to eq('inbound') }
            end
          end
        end
      end

      context 'when target project is same as the source project' do
        before do
          project.add_maintainer(current_user)
        end

        let(:target_project) { project }

        it_behaves_like 'returns error', "Validation failed: Target project can't be the same as the source project"
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  module JobTokenScope
    class RemoveProjectService < ::BaseService
      include EditScopeValidations

      def execute(target_project, direction)
        validate_edit!(project, target_project, current_user)

        if project == target_project
          return ServiceResponse.error(message: "Source project cannot be removed from the job token scope")
        end

        link = ::Ci::JobToken::ProjectScopeLink
          .with_access_direction(direction)
          .for_source_and_target(project, target_project)

        unless link
          return ServiceResponse.error(message: "Target project is not in the job token scope")
        end

        if link.destroy
          ServiceResponse.success
        else
          ServiceResponse.error(message: link.errors.full_messages.to_sentence, payload: { project_link: link })
        end
      rescue EditScopeValidations::ValidationError => e
        ServiceResponse.error(message: e.message, reason: :insufficient_permissions)
      end
    end
  end
end

Ci::JobTokenScope::RemoveProjectService.prepend_mod_with('Ci::JobTokenScope::RemoveProjectService')

```

Response:
# frozen_string_literal: true
require 'spec_helper'

RSpec.describe Ci::JobTokenScope::RemoveProjectService, feature_category: :continuous_integration do
  let(:service) { described_class.new(project, current_user) }

  let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
  let_it_be(:target_project) { create(:project) }
  let_it_be(:current_user) { create(:user) }

  let_it_be(:link) do
    create(:ci_job_token_project_scope_link,
      source_project: project,
      target_project: target_project)
  end

  shared_examples 'removes project' do |context|
    it 'removes the project from the scope' do
      expect do
        expect(result).to be_success
      end.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1)
    end
  end

  describe '#execute' do
    subject(:result) { service.execute(target_project, :outbound) }

    it_behaves_like 'editable job token scope' do
      context 'when user has permissions on source and target project' do
        before do
          project.add_maintainer(current_user)
          target_project.add_developer(current_user)
        end

        it_behaves_like 'removes project'

        context 'when token scope is disabled' do
          before do
            project.ci_cd_settings.update!(job_token_scope_enabled: false)
          end

          it_behaves_like 'removes project'
        end
      end

      context 'when target project is same as the source project' do
        before do
          project.add_maintainer(current_user)
        end

        let(:target_project) { project }

        it_behaves_like 'returns error', "Source project cannot be removed from the job token scope"
      end

      context 'when target project is not in the job token scope' do
        let_it_be(:target_project) { create(:project, :public) }

        before do
          project.add_maintainer(current_user)
        end

        it_behaves_like 'returns error', 'Target project is not in the job token scope'
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  module StuckBuilds
    class DropScheduledService
      include DropHelpers

      BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour

      def execute
        Gitlab::AppLogger.info "#{self.class}: Cleaning scheduled, timed-out builds"

        drop(scheduled_timed_out_builds, failure_reason: :stale_schedule)
      end

      private

      def scheduled_timed_out_builds
        Ci::Build.scheduled.scheduled_at_before(BUILD_SCHEDULED_OUTDATED_TIMEOUT.ago)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::StuckBuilds::DropScheduledService, feature_category: :runner_fleet do
  let_it_be(:runner) { create :ci_runner }

  let!(:job) { create :ci_build, :scheduled, scheduled_at: scheduled_at, runner: runner }

  subject(:service) { described_class.new }

  context 'when job is scheduled' do
    context 'for more than an hour ago' do
      let(:scheduled_at) { 2.hours.ago }

      it_behaves_like 'job is dropped with failure reason', 'stale_schedule'
    end

    context 'for less than 1 hour ago' do
      let(:scheduled_at) { 30.minutes.ago }

      it_behaves_like 'job is unchanged'
    end
  end

  %w[success skipped failed canceled running pending].each do |status|
    context "when job is #{status}" do
      before do
        job.update!(status: status)
      end

      context 'and scheduled for more than an hour ago' do
        let(:scheduled_at) { 2.hours.ago }

        it_behaves_like 'job is unchanged'
      end

      context 'and scheduled for less than 1 hour ago' do
        let(:scheduled_at) { 30.minutes.ago }

        it_behaves_like 'job is unchanged'
      end
    end
  end

  context 'when there are no stale scheduled builds' do
    let(:job) {}

    it 'does not drop the stale scheduled build yet' do
      expect { service.execute }.not_to raise_error
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Ci
  module StuckBuilds
    class DropPendingService
      include DropHelpers

      BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
      BUILD_PENDING_STUCK_TIMEOUT = 1.hour

      def execute
        Gitlab::AppLogger.info "#{self.class}: Cleaning pending timed-out builds"

        drop(
          pending_builds(BUILD_PENDING_OUTDATED_TIMEOUT.ago),
          failure_reason: :stuck_or_timeout_failure
        )

        drop_stuck(
          pending_builds(BUILD_PENDING_STUCK_TIMEOUT.ago),
          failure_reason: :stuck_or_timeout_failure
        )
      end

      private

      # rubocop: disable CodeReuse/ActiveRecord
      # We're adding the ordering clause by `created_at` and `project_id`
      # because we want to force the query planner to use the
      # `ci_builds_gitlab_monitor_metrics` index all the time.
      def pending_builds(timeout)
        Ci::Build
          .pending
          .created_at_before(timeout)
          .updated_at_before(timeout)
          .order(created_at: :asc, project_id: :asc)
      end
      # rubocop: enable CodeReuse/ActiveRecord
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::StuckBuilds::DropPendingService, feature_category: :runner_fleet do
  let_it_be(:runner) { create(:ci_runner) }
  let_it_be(:pipeline) { create(:ci_empty_pipeline) }
  let_it_be_with_reload(:job) do
    create(:ci_build, pipeline: pipeline, runner: runner)
  end

  let(:created_at) {}
  let(:updated_at) {}

  subject(:service) { described_class.new }

  before do
    job_attributes = { status: status }
    job_attributes[:created_at] = created_at if created_at
    job_attributes[:updated_at] = updated_at if updated_at
    job_attributes.compact!

    job.update!(job_attributes)
  end

  context 'when job is pending' do
    let(:status) { 'pending' }

    context 'when job is not stuck' do
      before do
        allow_next_found_instance_of(Ci::Build) do |build|
          allow(build).to receive(:stuck?).and_return(false)
        end
      end

      context 'when job was updated_at more than 1 day ago' do
        let(:updated_at) { 1.5.days.ago }

        context 'when created_at is the same as updated_at' do
          let(:created_at) { 1.5.days.ago }

          it_behaves_like 'job is dropped with failure reason', 'stuck_or_timeout_failure'
        end

        context 'when created_at is before updated_at' do
          let(:created_at) { 3.days.ago }

          it_behaves_like 'job is dropped with failure reason', 'stuck_or_timeout_failure'
        end
      end

      context 'when job was updated less than 1 day ago' do
        let(:updated_at) { 6.hours.ago }

        context 'when created_at is the same as updated_at' do
          let(:created_at) { 1.5.days.ago }

          it_behaves_like 'job is unchanged'
        end

        context 'when created_at is before updated_at' do
          let(:created_at) { 3.days.ago }

          it_behaves_like 'job is unchanged'
        end
      end

      context 'when job was updated more than 1 hour ago' do
        let(:updated_at) { 2.hours.ago }

        context 'when created_at is the same as updated_at' do
          let(:created_at) { 2.hours.ago }

          it_behaves_like 'job is unchanged'
        end

        context 'when created_at is before updated_at' do
          let(:created_at) { 3.days.ago }

          it_behaves_like 'job is unchanged'
        end
      end
    end

    context 'when job is stuck' do
      before do
        allow_next_found_instance_of(Ci::Build) do |build|
          allow(build).to receive(:stuck?).and_return(true)
        end
      end

      context 'when job was updated_at more than 1 hour ago' do
        let(:updated_at) { 1.5.hours.ago }

        context 'when created_at is the same as updated_at' do
          let(:created_at) { 1.5.hours.ago }

          it_behaves_like 'job is dropped with failure reason', 'stuck_or_timeout_failure'
        end

        context 'when created_at is before updated_at' do
          let(:created_at) { 3.days.ago }

          it_behaves_like 'job is dropped with failure reason', 'stuck_or_timeout_failure'
        end
      end

      context 'when job was updated in less than 1 hour ago' do
        let(:updated_at) { 30.minutes.ago }

        context 'when created_at is the same as updated_at' do
          let(:created_at) { 30.minutes.ago }

          it_behaves_like 'job is unchanged'
        end

        context 'when created_at is before updated_at' do
          let(:created_at) { 2.days.ago }

          it_behaves_like 'job is unchanged'
        end
      end
    end
  end

  context 'when job is running' do
    let(:status) { 'running' }

    context 'when job was updated_at more than an hour ago' do
      let(:updated_at) { 2.hours.ago }

      it_behaves_like 'job is unchanged'
    end

    context 'when job was updated in less than 1 hour ago' do
      let(:updated_at) { 30.minutes.ago }

      it_behaves_like 'job is unchanged'
    end
  end

  %w[success skipped failed canceled].each do |status|
    context "when job is #{status}" do
      let(:status) { status }
      let(:updated_at) { 2.days.ago }

      context 'when created_at is the same as updated_at' do
        let(:created_at) { 2.days.ago }

        it_behaves_like 'job is unchanged'
      end

      context 'when created_at is before updated_at' do
        let(:created_at) { 3.days.ago }

        it_behaves_like 'job is unchanged'
      end
    end
  end

  context 'for deleted project' do
    let(:status) { 'running' }
    let(:updated_at) { 2.days.ago }

    before do
      job.project.update!(pending_delete: true)
    end

    it_behaves_like 'job is unchanged'
  end
end