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

module Ci
  module StuckBuilds
    class DropRunningService
      include DropHelpers

      BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour

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

        drop(running_timed_out_builds, failure_reason: :stuck_or_timeout_failure)
      end

      private

      def running_timed_out_builds
        if Feature.enabled?(:ci_new_query_for_running_stuck_jobs)
          Ci::Build
            .running
            .created_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
            .updated_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
            .order(created_at: :asc, project_id: :asc) # rubocop:disable CodeReuse/ActiveRecord
        else
          Ci::Build.running.updated_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::StuckBuilds::DropRunningService, feature_category: :runner_fleet do
  let!(:runner) { create :ci_runner }
  let!(:job) { create(:ci_build, runner: runner, created_at: created_at, updated_at: updated_at, status: status) }

  subject(:service) { described_class.new }

  around do |example|
    freeze_time { example.run }
  end

  shared_examples 'running builds' do
    context 'when job is running' do
      let(:status) { 'running' }
      let(:outdated_time) { described_class::BUILD_RUNNING_OUTDATED_TIMEOUT.ago - 30.minutes }
      let(:fresh_time) { described_class::BUILD_RUNNING_OUTDATED_TIMEOUT.ago + 30.minutes }

      context 'when job is outdated' do
        let(:created_at) { outdated_time }
        let(:updated_at) { outdated_time }

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

      context 'when job is fresh' do
        let(:created_at) { fresh_time }
        let(:updated_at) { fresh_time }

        it_behaves_like 'job is unchanged'
      end

      context 'when job freshly updated' do
        let(:created_at) { outdated_time }
        let(:updated_at) { fresh_time }

        it_behaves_like 'job is unchanged'
      end
    end
  end

  include_examples 'running builds'

  context 'when new query flag is disabled' do
    before do
      stub_feature_flags(ci_new_query_for_running_stuck_jobs: false)
    end

    include_examples 'running builds'
  end

  %w[success skipped failed canceled scheduled pending].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
end


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

module Ci
  module ResourceGroups
    class AssignResourceFromResourceGroupService < ::BaseService
      # rubocop: disable CodeReuse/ActiveRecord
      def execute(resource_group)
        release_resource_from_stale_jobs(resource_group)

        free_resources = resource_group.resources.free.count

        resource_group.upcoming_processables.take(free_resources).each do |upcoming|
          Gitlab::OptimisticLocking.retry_lock(upcoming, name: 'enqueue_waiting_for_resource') do |processable|
            processable.enqueue_waiting_for_resource
          end
        end
      end
      # rubocop: enable CodeReuse/ActiveRecord

      private

      def release_resource_from_stale_jobs(resource_group)
        resource_group.resources.stale_processables.find_each do |processable|
          resource_group.release_resource_from(processable)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupService, feature_category: :continuous_integration do
  include ConcurrentHelpers

  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }

  let(:service) { described_class.new(project, user) }

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

    let(:resource_group) { create(:ci_resource_group, project: project) }
    let!(:build) { create(:ci_build, :waiting_for_resource, project: project, user: user, resource_group: resource_group) }

    context 'when there is an available resource' do
      it 'requests resource' do
        subject

        expect(build.reload).to be_pending
        expect(build.resource).to be_present
      end

      context 'when failed to request resource' do
        before do
          allow_next_instance_of(Ci::Build) do |build|
            allow(build).to receive(:enqueue_waiting_for_resource) { false }
          end
        end

        it 'has a build waiting for resource' do
          subject

          expect(build).to be_waiting_for_resource
        end
      end

      context 'when the build has already retained a resource' do
        before do
          resource_group.assign_resource_to(build)
          build.update_column(:status, :pending)
        end

        it 'has a pending build' do
          subject

          expect(build).to be_pending
        end
      end

      context 'when process mode is oldest_first' do
        let(:resource_group) { create(:ci_resource_group, process_mode: :oldest_first, project: project) }

        it 'requests resource' do
          subject

          expect(build.reload).to be_pending
          expect(build.resource).to be_present
        end

        context 'when the other job exists in the newer pipeline' do
          let!(:build_2) { create(:ci_build, :waiting_for_resource, project: project, user: user, resource_group: resource_group) }

          it 'requests resource for the job in the oldest pipeline' do
            subject

            expect(build.reload).to be_pending
            expect(build.resource).to be_present
            expect(build_2.reload).to be_waiting_for_resource
            expect(build_2.resource).to be_nil
          end
        end

        context 'when build is not `waiting_for_resource` state' do
          let!(:build) { create(:ci_build, :created, project: project, user: user, resource_group: resource_group) }

          it 'attempts to request a resource' do
            expect_next_found_instance_of(Ci::Build) do |job|
              expect(job).to receive(:enqueue_waiting_for_resource).and_call_original
            end

            subject
          end

          it 'does not change the job status' do
            subject

            expect(build.reload).to be_created
            expect(build.resource).to be_nil
          end
        end
      end

      context 'when process mode is newest_first' do
        let(:resource_group) { create(:ci_resource_group, process_mode: :newest_first, project: project) }

        it 'requests resource' do
          subject

          expect(build.reload).to be_pending
          expect(build.resource).to be_present
        end

        context 'when the other job exists in the newer pipeline' do
          let!(:build_2) { create(:ci_build, :waiting_for_resource, project: project, user: user, resource_group: resource_group) }

          it 'requests resource for the job in the newest pipeline' do
            subject

            expect(build.reload).to be_waiting_for_resource
            expect(build.resource).to be_nil
            expect(build_2.reload).to be_pending
            expect(build_2.resource).to be_present
          end
        end

        context 'when build is not `waiting_for_resource` state' do
          let!(:build) { create(:ci_build, :created, project: project, user: user, resource_group: resource_group) }

          it 'attempts to request a resource' do
            expect_next_found_instance_of(Ci::Build) do |job|
              expect(job).to receive(:enqueue_waiting_for_resource).and_call_original
            end

            subject
          end

          it 'does not change the job status' do
            subject

            expect(build.reload).to be_created
            expect(build.resource).to be_nil
          end
        end
      end

      context 'when parallel services are running' do
        it 'can run the same command in parallel' do
          parallel_num = 4

          blocks = Array.new(parallel_num).map do
            -> { subject }
          end

          run_parallel(blocks)
          expect(build.reload).to be_pending
        end
      end
    end

    context 'when there are no available resources' do
      let!(:other_build) { create(:ci_build) }

      before do
        resource_group.assign_resource_to(other_build)
      end

      it 'does not request resource' do
        expect_any_instance_of(Ci::Build).not_to receive(:enqueue_waiting_for_resource)

        subject

        expect(build.reload).to be_waiting_for_resource
      end

      context 'when there is a stale build assigned to a resource' do
        before do
          other_build.doom!
          other_build.update_column(:updated_at, 10.minutes.ago)
        end

        it 'releases the resource from the stale build and assignes to the waiting build' do
          subject

          expect(build.reload).to be_pending
          expect(build.resource).to be_present
        end
      end
    end
  end
end


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

module Ci
  module PipelineProcessing
    class AtomicProcessingService
      include Gitlab::Utils::StrongMemoize
      include ExclusiveLeaseGuard

      attr_reader :pipeline

      DEFAULT_LEASE_TIMEOUT = 1.minute
      BATCH_SIZE = 20

      def initialize(pipeline)
        @pipeline = pipeline
        @collection = AtomicProcessingService::StatusCollection.new(pipeline)
      end

      def execute
        return unless pipeline.needs_processing?

        # Run the process only if we can obtain an exclusive lease; returns nil if lease is unavailable
        success = try_obtain_lease { process! }

        if success
          # If any jobs changed from stopped to alive status during pipeline processing, we must
          # re-reset their dependent jobs; see https://gitlab.com/gitlab-org/gitlab/-/issues/388539.
          new_alive_jobs.group_by(&:user).each do |user, jobs|
            log_running_reset_skipped_jobs_service(jobs)

            ResetSkippedJobsService.new(project, user).execute(jobs)
          end

          # Re-schedule if we need further processing
          PipelineProcessWorker.perform_async(pipeline.id) if pipeline.needs_processing?
        end

        success
      end

      private

      def process!
        update_stages!
        update_pipeline!
        update_jobs_processed!

        Ci::ExpirePipelineCacheService.new.execute(pipeline)

        true
      end

      def update_stages!
        pipeline.stages.ordered.each { |stage| update_stage!(stage) }
      end

      def update_stage!(stage)
        # Update jobs for a given stage in bulk/slices
        @collection
          .created_job_ids_in_stage(stage.position)
          .in_groups_of(BATCH_SIZE, false) { |ids| update_jobs!(ids) }

        status = @collection.status_of_stage(stage.position)
        stage.set_status(status)
      end

      def update_jobs!(ids)
        created_jobs = pipeline
          .current_processable_jobs
          .id_in(ids)
          .with_project_preload
          .created
          .ordered_by_stage
          .select_with_aggregated_needs(project)

        created_jobs.each { |job| update_job!(job) }
      end

      def update_pipeline!
        pipeline.set_status(@collection.status_of_all)
      end

      def update_jobs_processed!
        processing = @collection.processing_jobs
        processing.each_slice(BATCH_SIZE) do |slice|
          pipeline.all_jobs.match_id_and_lock_version(slice)
            .update_as_processed!
        end
      end

      def update_job!(job)
        previous_status = status_of_previous_jobs(job)
        # We do not continue to process the job if the previous status is not completed
        return unless Ci::HasStatus::COMPLETED_STATUSES.include?(previous_status)

        ::Deployments::CreateForJobService.new.execute(job)

        Gitlab::OptimisticLocking.retry_lock(job, name: 'atomic_processing_update_job') do |subject|
          Ci::ProcessBuildService.new(project, subject.user)
            .execute(subject, previous_status)

          # update internal representation of job
          # to make the status change of job to be taken into account during further processing
          @collection.set_job_status(job.id, job.status, job.lock_version)
        end
      end

      def status_of_previous_jobs(job)
        if job.scheduling_type_dag?
          # job uses DAG, get status of all dependent needs
          @collection.status_of_jobs(job.aggregated_needs_names.to_a)
        else
          # job uses Stages, get status of prior stage
          @collection.status_of_jobs_prior_to_stage(job.stage_idx.to_i)
        end
      end

      # Gets the jobs that changed from stopped to alive status since the initial status collection
      # was evaluated. We determine this by checking if their current status is no longer stopped.
      def new_alive_jobs
        initial_stopped_job_names = @collection.stopped_job_names

        return [] if initial_stopped_job_names.empty?

        new_collection = AtomicProcessingService::StatusCollection.new(pipeline)
        new_alive_job_names = initial_stopped_job_names - new_collection.stopped_job_names

        return [] if new_alive_job_names.empty?

        pipeline
          .current_jobs
          .by_name(new_alive_job_names)
          .preload(:user) # rubocop: disable CodeReuse/ActiveRecord
          .to_a
      end

      def project
        pipeline.project
      end

      def lease_key
        "#{super}::pipeline_id:#{pipeline.id}"
      end

      def lease_timeout
        DEFAULT_LEASE_TIMEOUT
      end

      def lease_taken_log_level
        :info
      end

      def log_running_reset_skipped_jobs_service(jobs)
        Gitlab::AppJsonLogger.info(
          class: self.class.name.to_s,
          message: 'Running ResetSkippedJobsService on new alive jobs',
          project_id: project.id,
          pipeline_id: pipeline.id,
          user_id: jobs.first.user.id,
          jobs_count: jobs.count
        )
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineProcessing::AtomicProcessingService, feature_category: :continuous_integration do
  include RepoHelpers
  include ExclusiveLeaseHelpers

  describe 'Pipeline Processing Service Tests With Yaml' do
    let_it_be(:project) { create(:project, :repository) }
    let_it_be(:user)    { project.first_owner }

    where(:test_file_path) do
      Dir.glob(Rails.root.join('spec/services/ci/pipeline_processing/test_cases/*.yml'))
    end

    with_them do
      let(:test_file) { YAML.load_file(test_file_path) }
      let(:pipeline) { Ci::CreatePipelineService.new(project, user, ref: 'master').execute(:pipeline).payload }

      before do
        stub_ci_pipeline_yaml_file(YAML.dump(test_file['config']))
      end

      it 'follows transitions' do
        expect(pipeline).to be_persisted
        Sidekiq::Worker.drain_all # ensure that all async jobs are executed
        check_expectation(test_file.dig('init', 'expect'), "init")

        test_file['transitions'].each_with_index do |transition, idx|
          process_events(transition)
          Sidekiq::Worker.drain_all # ensure that all async jobs are executed
          check_expectation(transition['expect'], "transition:#{idx}")
        end
      end

      private

      def check_expectation(expectation, message)
        expect(current_state.deep_stringify_keys).to eq(expectation), message
      end

      def current_state
        # reload pipeline and all relations
        pipeline.reload

        {
          pipeline: pipeline.status,
          stages: pipeline.stages.pluck(:name, :status).to_h,
          jobs: pipeline.latest_statuses.pluck(:name, :status).to_h
        }
      end

      def process_events(transition)
        if transition['jobs']
          event_on_jobs(transition['event'], transition['jobs'])
        else
          event_on_pipeline(transition['event'])
        end
      end

      def event_on_jobs(event, job_names)
        jobs = pipeline.latest_statuses.by_name(job_names).to_a
        expect(jobs.count).to eq(job_names.count) # ensure that we have the same counts

        jobs.each do |job|
          case event
          when 'play'
            job.play(user)
          when 'retry'
            ::Ci::RetryJobService.new(project, user).execute(job)
          else
            job.public_send("#{event}!")
          end
        end
      end

      def event_on_pipeline(event)
        if event == 'retry'
          pipeline.retry_failed(user)
        else
          pipeline.public_send("#{event}!")
        end
      end
    end
  end

  describe 'Pipeline Processing Service' do
    let(:project) { create(:project, :repository) }
    let(:user)    { project.first_owner }

    let(:pipeline) do
      create(:ci_empty_pipeline, ref: 'master', project: project)
    end

    context 'when simple pipeline is defined' do
      before do
        create_build('linux', stage_idx: 0)
        create_build('mac', stage_idx: 0)
        create_build('rspec', stage_idx: 1)
        create_build('rubocop', stage_idx: 1)
        create_build('deploy', stage_idx: 2)
      end

      it 'processes a pipeline', :sidekiq_inline do
        expect(process_pipeline).to be_truthy

        succeed_pending

        expect(builds.success.count).to eq(2)

        succeed_pending

        expect(builds.success.count).to eq(4)

        succeed_pending

        expect(builds.success.count).to eq(5)
      end

      it 'does not process pipeline if existing stage is running' do
        expect(process_pipeline).to be_truthy
        expect(builds.pending.count).to eq(2)

        expect(process_pipeline).to be_falsey
        expect(builds.pending.count).to eq(2)
      end
    end

    context 'custom stage with first job allowed to fail' do
      before do
        create_build('clean_job', stage_idx: 0, allow_failure: true)
        create_build('test_job', stage_idx: 1, allow_failure: true)
      end

      it 'automatically triggers a next stage when build finishes', :sidekiq_inline do
        expect(process_pipeline).to be_truthy
        expect(builds_statuses).to eq ['pending']

        fail_running_or_pending

        expect(builds_statuses).to eq %w[failed pending]

        fail_running_or_pending

        expect(pipeline.reload).to be_success
      end
    end

    context 'when optional manual actions are defined', :sidekiq_inline do
      before do
        create_build('build', stage_idx: 0)
        create_build('test', stage_idx: 1)
        create_build('test_failure', stage_idx: 2, when: 'on_failure')
        create_build('deploy', stage_idx: 3)
        create_build('production', stage_idx: 3, when: 'manual', allow_failure: true)
        create_build('cleanup', stage_idx: 4, when: 'always')
        create_build('clear:cache', stage_idx: 4, when: 'manual', allow_failure: true)
      end

      context 'when builds are successful' do
        it 'properly processes the pipeline' do
          expect(process_pipeline).to be_truthy
          expect(builds_names).to eq ['build']
          expect(builds_statuses).to eq ['pending']

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test]
          expect(builds_statuses).to eq %w[success pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test deploy production]
          expect(builds_statuses).to eq %w[success success pending manual]

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test deploy production cleanup clear:cache]
          expect(builds_statuses).to eq %w[success success success manual pending manual]

          succeed_running_or_pending

          expect(builds_statuses).to eq %w[success success success manual success manual]
          expect(pipeline.reload.status).to eq 'success'
        end
      end

      context 'when test job fails' do
        it 'properly processes the pipeline' do
          expect(process_pipeline).to be_truthy
          expect(builds_names).to eq ['build']
          expect(builds_statuses).to eq ['pending']

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test]
          expect(builds_statuses).to eq %w[success pending]

          fail_running_or_pending

          expect(builds_names).to eq %w[build test test_failure]
          expect(builds_statuses).to eq %w[success failed pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test test_failure cleanup]
          expect(builds_statuses).to eq %w[success failed success pending]

          succeed_running_or_pending

          expect(builds_statuses).to eq %w[success failed success success]
          expect(pipeline.reload.status).to eq 'failed'
        end
      end

      context 'when test and test_failure jobs fail' do
        it 'properly processes the pipeline' do
          expect(process_pipeline).to be_truthy
          expect(builds_names).to eq ['build']
          expect(builds_statuses).to eq ['pending']

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test]
          expect(builds_statuses).to eq %w[success pending]

          fail_running_or_pending

          expect(builds_names).to eq %w[build test test_failure]
          expect(builds_statuses).to eq %w[success failed pending]

          fail_running_or_pending

          expect(builds_names).to eq %w[build test test_failure cleanup]
          expect(builds_statuses).to eq %w[success failed failed pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test test_failure cleanup]
          expect(builds_statuses).to eq %w[success failed failed success]
          expect(pipeline.reload.status).to eq('failed')
        end
      end

      context 'when deploy job fails' do
        it 'properly processes the pipeline' do
          expect(process_pipeline).to be_truthy
          expect(builds_names).to eq ['build']
          expect(builds_statuses).to eq ['pending']

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test]
          expect(builds_statuses).to eq %w[success pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[build test deploy production]
          expect(builds_statuses).to eq %w[success success pending manual]

          fail_running_or_pending

          expect(builds_names).to eq %w[build test deploy production cleanup]
          expect(builds_statuses).to eq %w[success success failed manual pending]

          succeed_running_or_pending

          expect(builds_statuses).to eq %w[success success failed manual success]
          expect(pipeline.reload).to be_failed
        end
      end

      context 'when build is canceled in the second stage' do
        it 'does not schedule builds after build has been canceled' do
          expect(process_pipeline).to be_truthy
          expect(builds_names).to eq ['build']
          expect(builds_statuses).to eq ['pending']

          succeed_running_or_pending

          expect(builds.running_or_pending).not_to be_empty
          expect(builds_names).to eq %w[build test]
          expect(builds_statuses).to eq %w[success pending]

          cancel_running_or_pending

          expect(builds.running_or_pending).to be_empty
          expect(builds_names).to eq %w[build test]
          expect(builds_statuses).to eq %w[success canceled]
          expect(pipeline.reload).to be_canceled
        end
      end

      context 'when listing optional manual actions' do
        it 'returns only for skipped builds' do
          # currently all builds are created
          expect(process_pipeline).to be_truthy
          expect(manual_actions).to be_empty

          # succeed stage build
          succeed_running_or_pending

          expect(manual_actions).to be_empty

          # succeed stage test
          succeed_running_or_pending

          expect(manual_actions).to be_one # production

          # succeed stage deploy
          succeed_running_or_pending

          expect(manual_actions).to be_many # production and clear cache
        end
      end
    end

    context 'when delayed jobs are defined', :sidekiq_inline do
      context 'when the scene is timed incremental rollout' do
        before do
          create_build('build', stage_idx: 0)
          create_build('rollout10%', **delayed_options, stage_idx: 1)
          create_build('rollout100%', **delayed_options, stage_idx: 2)
          create_build('cleanup', stage_idx: 3)

          allow(Ci::BuildScheduleWorker).to receive(:perform_at)
        end

        context 'when builds are successful' do
          it 'properly processes the pipeline' do
            expect(process_pipeline).to be_truthy
            expect(builds_names_and_statuses).to eq({ 'build': 'pending' })

            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })

            travel_to 2.minutes.from_now do
              enqueue_scheduled('rollout10%')
            end
            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'scheduled' })

            travel_to 2.minutes.from_now do
              enqueue_scheduled('rollout100%')
            end
            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'success', 'cleanup': 'pending' })

            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'success', 'cleanup': 'success' })
            expect(pipeline.reload.status).to eq 'success'
          end
        end

        context 'when build job fails' do
          it 'properly processes the pipeline' do
            expect(process_pipeline).to be_truthy
            expect(builds_names_and_statuses).to eq({ 'build': 'pending' })

            fail_running_or_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'failed' })
            expect(pipeline.reload.status).to eq 'failed'
          end
        end

        context 'when rollout 10% is unscheduled' do
          it 'properly processes the pipeline' do
            expect(process_pipeline).to be_truthy
            expect(builds_names_and_statuses).to eq({ 'build': 'pending' })

            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })

            unschedule

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'manual' })
            expect(pipeline.reload.status).to eq 'manual'
          end

          context 'when user plays rollout 10%' do
            it 'schedules rollout100%' do
              process_pipeline
              succeed_pending
              unschedule
              play_manual_action('rollout10%')
              succeed_pending

              expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'scheduled' })
              expect(pipeline.reload.status).to eq 'scheduled'
            end
          end
        end

        context 'when rollout 10% fails' do
          it 'properly processes the pipeline' do
            expect(process_pipeline).to be_truthy
            expect(builds_names_and_statuses).to eq({ 'build': 'pending' })

            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })

            travel_to 2.minutes.from_now do
              enqueue_scheduled('rollout10%')
            end
            fail_running_or_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'failed' })
            expect(pipeline.reload.status).to eq 'failed'
          end

          context 'when user retries rollout 10%' do
            it 'does not schedule rollout10% again' do
              process_pipeline
              succeed_pending
              enqueue_scheduled('rollout10%')
              fail_running_or_pending
              retry_build('rollout10%')

              expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'pending' })
              expect(pipeline.reload.status).to eq 'running'
            end
          end
        end

        context 'when rollout 10% is played immidiately' do
          it 'properly processes the pipeline' do
            expect(process_pipeline).to be_truthy
            expect(builds_names_and_statuses).to eq({ 'build': 'pending' })

            succeed_pending

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })

            play_manual_action('rollout10%')

            expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'pending' })
            expect(pipeline.reload.status).to eq 'running'
          end
        end
      end

      context 'when only one scheduled job exists in a pipeline' do
        before do
          create_build('delayed', **delayed_options, stage_idx: 0)

          allow(Ci::BuildScheduleWorker).to receive(:perform_at)
        end

        it 'properly processes the pipeline' do
          expect(process_pipeline).to be_truthy
          expect(builds_names_and_statuses).to eq({ 'delayed': 'scheduled' })

          expect(pipeline.reload.status).to eq 'scheduled'
        end
      end

      context 'when there are two delayed jobs in a stage' do
        before do
          create_build('delayed1', **delayed_options, stage_idx: 0)
          create_build('delayed2', **delayed_options, stage_idx: 0)
          create_build('job', stage_idx: 1)

          allow(Ci::BuildScheduleWorker).to receive(:perform_at)
        end

        it 'blocks the stage until all scheduled jobs finished' do
          expect(process_pipeline).to be_truthy
          expect(builds_names_and_statuses).to eq({ 'delayed1': 'scheduled', 'delayed2': 'scheduled' })

          travel_to 2.minutes.from_now do
            enqueue_scheduled('delayed1')
          end

          expect(builds_names_and_statuses).to eq({ 'delayed1': 'pending', 'delayed2': 'scheduled' })
          expect(pipeline.reload.status).to eq 'running'
        end
      end

      context 'when a delayed job is allowed to fail' do
        before do
          create_build('delayed', **delayed_options, allow_failure: true, stage_idx: 0)
          create_build('job', stage_idx: 1)

          allow(Ci::BuildScheduleWorker).to receive(:perform_at)
        end

        it 'blocks the stage and continues after it failed' do
          expect(process_pipeline).to be_truthy
          expect(builds_names_and_statuses).to eq({ 'delayed': 'scheduled' })

          travel_to 2.minutes.from_now do
            enqueue_scheduled('delayed')
          end
          fail_running_or_pending

          expect(builds_names_and_statuses).to eq({ 'delayed': 'failed', 'job': 'pending' })
          expect(pipeline.reload.status).to eq 'pending'
        end
      end
    end

    context 'when an exception is raised during a persistent ref creation' do
      before do
        successful_build('test', stage_idx: 0)

        allow_next_instance_of(Ci::PersistentRef) do |instance|
          allow(instance).to receive(:delete_refs) { raise ArgumentError }
        end
      end

      it 'process the pipeline' do
        expect { process_pipeline }.not_to raise_error
      end
    end

    context 'when there are manual action in earlier stages' do
      context 'when first stage has only optional manual actions' do
        before do
          create_build('build', stage_idx: 0, when: 'manual', allow_failure: true)
          create_build('check', stage_idx: 1)
          create_build('test', stage_idx: 2)

          process_pipeline
        end

        it 'starts from the second stage' do
          expect(all_builds_statuses).to eq %w[manual pending created]
        end
      end

      context 'when second stage has only optional manual actions' do
        before do
          create_build('check', stage_idx: 0)
          create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
          create_build('test', stage_idx: 2)

          process_pipeline
        end

        it 'skips second stage and continues on third stage', :sidekiq_inline do
          expect(all_builds_statuses).to eq(%w[pending created created])

          builds.first.success

          expect(all_builds_statuses).to eq(%w[success manual pending])
        end
      end
    end

    context 'when there are only manual actions in stages' do
      before do
        create_build('image', stage_idx: 0, when: 'manual', allow_failure: true)
        create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
        create_build('deploy', stage_idx: 2, when: 'manual')
        create_build('check', stage_idx: 3)

        process_pipeline
      end

      it 'processes all jobs until blocking actions encountered' do
        expect(all_builds_statuses).to eq(%w[manual manual manual created])
        expect(all_builds_names).to eq(%w[image build deploy check])

        expect(pipeline.reload).to be_blocked
      end
    end

    context 'when there is only one manual action' do
      before do
        create_build('deploy', stage_idx: 0, when: 'manual', allow_failure: true)

        process_pipeline
      end

      it 'skips the pipeline' do
        expect(pipeline.reload).to be_skipped
      end

      context 'when the action was played' do
        before do
          play_manual_action('deploy')
        end

        it 'queues the action and pipeline', :sidekiq_inline do
          expect(all_builds_statuses).to eq(%w[pending])

          expect(pipeline.reload).to be_pending
        end
      end
    end

    context 'when blocking manual actions are defined', :sidekiq_inline do
      before do
        create_build('code:test', stage_idx: 0)
        create_build('staging:deploy', stage_idx: 1, when: 'manual')
        create_build('staging:test', stage_idx: 2, when: 'on_success')
        create_build('production:deploy', stage_idx: 3, when: 'manual')
        create_build('production:test', stage_idx: 4, when: 'always')
      end

      context 'when first stage succeeds' do
        it 'blocks pipeline on stage with first manual action' do
          process_pipeline

          expect(builds_names).to eq %w[code:test]
          expect(builds_statuses).to eq %w[pending]
          expect(pipeline.reload.status).to eq 'pending'

          succeed_running_or_pending

          expect(builds_names).to eq %w[code:test staging:deploy]
          expect(builds_statuses).to eq %w[success manual]
          expect(pipeline.reload).to be_manual
        end
      end

      context 'when first stage fails' do
        it 'does not take blocking action into account' do
          process_pipeline

          expect(builds_names).to eq %w[code:test]
          expect(builds_statuses).to eq %w[pending]
          expect(pipeline.reload.status).to eq 'pending'

          fail_running_or_pending

          expect(builds_names).to eq %w[code:test production:test]
          expect(builds_statuses).to eq %w[failed pending]

          succeed_running_or_pending

          expect(builds_statuses).to eq %w[failed success]
          expect(pipeline.reload).to be_failed
        end
      end

      context 'when pipeline is promoted sequentially up to the end' do
        before do
          # Users need ability to merge into a branch in order to trigger
          # protected manual actions.
          #
          create(:protected_branch, :developers_can_merge, name: 'master', project: project)
        end

        it 'properly processes entire pipeline' do
          process_pipeline

          expect(builds_names).to eq %w[code:test]
          expect(builds_statuses).to eq %w[pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[code:test staging:deploy]
          expect(builds_statuses).to eq %w[success manual]
          expect(pipeline.reload).to be_manual

          play_manual_action('staging:deploy')

          expect(builds_statuses).to eq %w[success pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[code:test staging:deploy staging:test]
          expect(builds_statuses).to eq %w[success success pending]

          succeed_running_or_pending

          expect(builds_names).to eq %w[code:test staging:deploy staging:test
                                        production:deploy]
          expect(builds_statuses).to eq %w[success success success manual]

          expect(pipeline.reload).to be_manual
          expect(pipeline.reload).to be_blocked
          expect(pipeline.reload).not_to be_active
          expect(pipeline.reload).not_to be_complete

          play_manual_action('production:deploy')

          expect(builds_statuses).to eq %w[success success success pending]
          expect(pipeline.reload).to be_running

          succeed_running_or_pending

          expect(builds_names).to eq %w[code:test staging:deploy staging:test
                                        production:deploy production:test]
          expect(builds_statuses).to eq %w[success success success success pending]
          expect(pipeline.reload).to be_running

          succeed_running_or_pending

          expect(builds_names).to eq %w[code:test staging:deploy staging:test
                                        production:deploy production:test]
          expect(builds_statuses).to eq %w[success success success success success]
          expect(pipeline.reload).to be_success
        end
      end
    end

    context 'when second stage has only on_failure jobs', :sidekiq_inline do
      before do
        create_build('check', stage_idx: 0)
        create_build('build', stage_idx: 1, when: 'on_failure')
        create_build('test', stage_idx: 2)

        process_pipeline
      end

      it 'skips second stage and continues on third stage' do
        expect(all_builds_statuses).to eq(%w[pending created created])

        builds.first.success

        expect(all_builds_statuses).to eq(%w[success skipped pending])
      end
    end

    context 'when failed build in the middle stage is retried', :sidekiq_inline do
      context 'when failed build is the only unsuccessful build in the stage' do
        before do
          create_build('build:1', stage_idx: 0)
          create_build('build:2', stage_idx: 0)
          create_build('test:1', stage_idx: 1)
          create_build('test:2', stage_idx: 1)
          create_build('deploy:1', stage_idx: 2)
          create_build('deploy:2', stage_idx: 2)
        end

        it 'does trigger builds in the next stage' do
          expect(process_pipeline).to be_truthy
          expect(builds_names).to eq ['build:1', 'build:2']

          succeed_running_or_pending

          expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']

          pipeline.builds.find_by(name: 'test:1').success!
          pipeline.builds.find_by(name: 'test:2').drop!

          expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']

          Ci::RetryJobService.new(pipeline.project, user).execute(pipeline.builds.find_by(name: 'test:2'))[:job].reset.success!

          expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2',
                                      'test:2', 'deploy:1', 'deploy:2']
        end
      end
    end

    context 'when builds with auto-retries are configured', :sidekiq_inline do
      before do
        create_build('build:1', stage_idx: 0, user: user, options: { script: 'aa', retry: 2 })
        create_build('test:1', stage_idx: 1, user: user, when: :on_failure)
        create_build('test:2', stage_idx: 1, user: user, options: { script: 'aa', retry: 1 })
      end

      it 'automatically retries builds in a valid order' do
        expect(process_pipeline).to be_truthy

        fail_running_or_pending

        expect(builds_names).to eq %w[build:1 build:1]
        expect(builds_statuses).to eq %w[failed pending]

        succeed_running_or_pending

        expect(builds_names).to eq %w[build:1 build:1 test:2]
        expect(builds_statuses).to eq %w[failed success pending]

        succeed_running_or_pending

        expect(builds_names).to eq %w[build:1 build:1 test:2]
        expect(builds_statuses).to eq %w[failed success success]

        expect(pipeline.reload).to be_success
      end
    end

    context 'when pipeline with needs is created', :sidekiq_inline do
      let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0) }
      let!(:mac_build) { create_build('mac:build', stage: 'build', stage_idx: 0) }
      let!(:linux_rspec) { create_build('linux:rspec', stage: 'test', stage_idx: 1, scheduling_type: :dag) }
      let!(:linux_rubocop) { create_build('linux:rubocop', stage: 'test', stage_idx: 1, scheduling_type: :dag) }
      let!(:mac_rspec) { create_build('mac:rspec', stage: 'test', stage_idx: 1, scheduling_type: :dag) }
      let!(:mac_rubocop) { create_build('mac:rubocop', stage: 'test', stage_idx: 1, scheduling_type: :dag) }
      let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 2) }

      let!(:linux_rspec_on_build) { create(:ci_build_need, build: linux_rspec, name: 'linux:build') }
      let!(:linux_rubocop_on_build) { create(:ci_build_need, build: linux_rubocop, name: 'linux:build') }

      let!(:mac_rspec_on_build) { create(:ci_build_need, build: mac_rspec, name: 'mac:build') }
      let!(:mac_rubocop_on_build) { create(:ci_build_need, build: mac_rubocop, name: 'mac:build') }

      it 'when linux:* finishes first it runs it out of order' do
        expect(process_pipeline).to be_truthy

        expect(stages).to eq(%w[pending created created])
        expect(builds.pending).to contain_exactly(linux_build, mac_build)

        # we follow the single path of linux
        linux_build.reset.success!

        expect(stages).to eq(%w[running pending created])
        expect(builds.success).to contain_exactly(linux_build)
        expect(builds.pending).to contain_exactly(mac_build, linux_rspec, linux_rubocop)

        linux_rspec.reset.success!

        expect(stages).to eq(%w[running running created])
        expect(builds.success).to contain_exactly(linux_build, linux_rspec)
        expect(builds.pending).to contain_exactly(mac_build, linux_rubocop)

        linux_rubocop.reset.success!

        expect(stages).to eq(%w[running running created])
        expect(builds.success).to contain_exactly(linux_build, linux_rspec, linux_rubocop)
        expect(builds.pending).to contain_exactly(mac_build)

        mac_build.reset.success!
        mac_rspec.reset.success!
        mac_rubocop.reset.success!

        expect(stages).to eq(%w[success success pending])
        expect(builds.success).to contain_exactly(
          linux_build, linux_rspec, linux_rubocop, mac_build, mac_rspec, mac_rubocop)
        expect(builds.pending).to contain_exactly(deploy)
      end

      context 'when one of the jobs is run on a failure' do
        let!(:linux_notify) { create_build('linux:notify', stage: 'deploy', stage_idx: 2, when: 'on_failure', scheduling_type: :dag) }

        let!(:linux_notify_on_build) { create(:ci_build_need, build: linux_notify, name: 'linux:build') }

        context 'when another job in build phase fails first' do
          it 'does skip linux:notify' do
            expect(process_pipeline).to be_truthy

            mac_build.reset.drop!
            linux_build.reset.success!

            expect(linux_notify.reset).to be_skipped
          end
        end

        context 'when linux:build job fails first' do
          it 'does run linux:notify' do
            expect(process_pipeline).to be_truthy

            linux_build.reset.drop!

            expect(linux_notify.reset).to be_pending
          end
        end
      end

      context 'when there is a job scheduled with dag but no need (needs: [])' do
        let!(:deploy_pages) { create_build('deploy_pages', stage: 'deploy', stage_idx: 2, scheduling_type: :dag) }

        it 'runs deploy_pages without waiting prior stages' do
          expect(process_pipeline).to be_truthy

          expect(stages).to eq(%w[pending created pending])
          expect(builds.pending).to contain_exactly(linux_build, mac_build, deploy_pages)

          linux_build.reset.success!
          deploy_pages.reset.success!

          expect(stages).to eq(%w[running pending running])
          expect(builds.success).to contain_exactly(linux_build, deploy_pages)
          expect(builds.pending).to contain_exactly(mac_build, linux_rspec, linux_rubocop)

          linux_rspec.reset.success!
          linux_rubocop.reset.success!
          mac_build.reset.success!
          mac_rspec.reset.success!
          mac_rubocop.reset.success!

          expect(stages).to eq(%w[success success running])
          expect(builds.pending).to contain_exactly(deploy)
        end
      end
    end

    context 'when a needed job is skipped', :sidekiq_inline do
      let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0) }
      let!(:linux_rspec) { create_build('linux:rspec', stage: 'test', stage_idx: 1) }
      let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 2, scheduling_type: :dag) }

      before do
        create(:ci_build_need, build: deploy, name: 'linux:build')
      end

      it 'skips the jobs depending on it' do
        expect(process_pipeline).to be_truthy

        expect(stages).to eq(%w[pending created created])
        expect(all_builds.pending).to contain_exactly(linux_build)

        linux_build.reset.drop!

        expect(stages).to eq(%w[failed skipped skipped])
        expect(all_builds.failed).to contain_exactly(linux_build)
        expect(all_builds.skipped).to contain_exactly(linux_rspec, deploy)
      end
    end

    context 'when a needed job is manual', :sidekiq_inline do
      let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0, when: 'manual', allow_failure: true) }
      let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 1, scheduling_type: :dag) }

      before do
        create(:ci_build_need, build: deploy, name: 'linux:build')
      end

      it 'makes deploy DAG to be skipped' do
        expect(process_pipeline).to be_truthy

        expect(stages).to eq(%w[skipped skipped])
        expect(all_builds.manual).to contain_exactly(linux_build)
        expect(all_builds.skipped).to contain_exactly(deploy)
      end
    end

    context 'when jobs change from stopped to alive status during pipeline processing' do
      around do |example|
        Sidekiq::Testing.fake! { example.run }
      end

      let(:config) do
        <<-YAML
        stages: [test, deploy]

        manual1:
          stage: test
          when: manual
          script: exit 0

        manual2:
          stage: test
          when: manual
          script: exit 0

        test1:
          stage: test
          needs: [manual1]
          script: exit 0

        test2:
          stage: test
          needs: [manual2]
          script: exit 0

        deploy1:
          stage: deploy
          needs: [manual1, manual2]
          script: exit 0

        deploy2:
          stage: deploy
          needs: [test2]
          script: exit 0
        YAML
      end

      let(:pipeline) do
        Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
      end

      let(:manual1) { all_builds.find_by(name: 'manual1') }
      let(:manual2) { all_builds.find_by(name: 'manual2') }

      let(:statuses_0) do
        { 'manual1': 'created', 'manual2': 'created', 'test1': 'created', 'test2': 'created', 'deploy1': 'created', 'deploy2': 'created' }
      end

      let(:statuses_1) do
        { 'manual1': 'manual', 'manual2': 'manual', 'test1': 'skipped', 'test2': 'skipped', 'deploy1': 'skipped', 'deploy2': 'skipped' }
      end

      let(:statuses_2) do
        { 'manual1': 'pending', 'manual2': 'pending', 'test1': 'skipped', 'test2': 'skipped', 'deploy1': 'skipped', 'deploy2': 'skipped' }
      end

      let(:statuses_3) do
        { 'manual1': 'pending', 'manual2': 'pending', 'test1': 'created', 'test2': 'created', 'deploy1': 'created', 'deploy2': 'created' }
      end

      let(:log_info) do
        {
          class: described_class.name.to_s,
          message: 'Running ResetSkippedJobsService on new alive jobs',
          project_id: project.id,
          pipeline_id: pipeline.id,
          user_id: user.id,
          jobs_count: 2
        }
      end

      before do
        stub_ci_pipeline_yaml_file(config)
        pipeline # Create the pipeline
      end

      # Since this is a test for a race condition, we are calling internal method `enqueue!`
      # instead of `play` and stubbing `new_alive_jobs` of the service class.
      it 'runs ResetSkippedJobsService on the new alive jobs and logs event' do
        # Initial control without any pipeline processing
        expect(all_builds_names_and_statuses).to eq(statuses_0)

        process_pipeline

        # Initial control after the first pipeline processing
        expect(all_builds_names_and_statuses).to eq(statuses_1)

        # Change the manual jobs from stopped to alive status.
        # We don't use `play` to avoid running `ResetSkippedJobsService`.
        manual1.enqueue!
        manual2.enqueue!

        # Statuses after playing the manual jobs
        expect(all_builds_names_and_statuses).to eq(statuses_2)

        mock_play_jobs_during_processing([manual1, manual2])

        expect(Ci::ResetSkippedJobsService).to receive(:new).once.and_call_original

        process_pipeline

        expect(all_builds_names_and_statuses).to eq(statuses_3)
      end

      it 'logs event' do
        expect(Gitlab::AppJsonLogger).to receive(:info).once.with(log_info)

        mock_play_jobs_during_processing([manual1, manual2])
        process_pipeline
      end

      context 'when the new alive jobs belong to different users' do
        let_it_be(:user2) { create(:user) }

        before do
          process_pipeline # First pipeline processing

          # Change the manual jobs from stopped to alive status
          manual1.enqueue!
          manual2.enqueue!

          manual2.update!(user: user2)

          mock_play_jobs_during_processing([manual1, manual2])
        end

        it 'runs ResetSkippedJobsService on the new alive jobs' do
          # Statuses after playing the manual jobs
          expect(all_builds_names_and_statuses).to eq(statuses_2)

          # Since there are two different users, we expect this service to be called twice.
          expect(Ci::ResetSkippedJobsService).to receive(:new).twice.and_call_original

          process_pipeline

          expect(all_builds_names_and_statuses).to eq(statuses_3)
        end

        # In this scenario, the new alive jobs (manual1 and manual2) have different users.
        # We can only know for certain the assigned user of dependent jobs that are exclusive
        # to either manual1 or manual2. Otherwise, the assigned user will depend on which of
        # the new alive jobs get processed first by ResetSkippedJobsService.
        it 'assigns the correct user to the dependent jobs' do
          test1 = all_builds.find_by(name: 'test1')
          test2 = all_builds.find_by(name: 'test2')

          expect(test1.user).to eq(user)
          expect(test2.user).to eq(user)

          process_pipeline

          expect(test1.reset.user).to eq(user)
          expect(test2.reset.user).to eq(user2)
        end

        it 'logs event' do
          expect(Gitlab::AppJsonLogger).to receive(:info).once.with(log_info.merge(jobs_count: 1))
          expect(Gitlab::AppJsonLogger).to receive(:info).once.with(log_info.merge(user_id: user2.id, jobs_count: 1))

          mock_play_jobs_during_processing([manual1, manual2])
          process_pipeline
        end
      end
    end

    context 'when a bridge job has parallel:matrix config', :sidekiq_inline do
      let(:parent_config) do
        <<-EOY
        test:
          stage: test
          script: echo test

        deploy:
          stage: deploy
          trigger:
            include: .child.yml
          parallel:
            matrix:
              - PROVIDER: ovh
                STACK: [monitoring, app]
        EOY
      end

      let(:child_config) do
        <<-EOY
        test:
          stage: test
          script: echo test
        EOY
      end

      let(:pipeline) do
        Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
      end

      let(:project_files) do
        {
          '.gitlab-ci.yml' => parent_config,
          '.child.yml' => child_config
        }
      end

      around do |example|
        create_and_delete_files(project, project_files) do
          example.run
        end
      end

      it 'creates pipeline with bridges, then passes the matrix variables to downstream jobs' do
        expect(all_builds_names).to contain_exactly('test', 'deploy: [ovh, monitoring]', 'deploy: [ovh, app]')
        expect(all_builds_statuses).to contain_exactly('pending', 'created', 'created')

        succeed_pending

        # bridge jobs directly transition to success
        expect(all_builds_statuses).to contain_exactly('success', 'success', 'success')

        bridge1 = all_builds.find_by(name: 'deploy: [ovh, monitoring]')
        bridge2 = all_builds.find_by(name: 'deploy: [ovh, app]')

        downstream_job1 = bridge1.downstream_pipeline.all_jobs.first
        downstream_job2 = bridge2.downstream_pipeline.all_jobs.first

        expect(downstream_job1.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'monitoring')
        expect(downstream_job2.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'app')
      end
    end

    context 'when a bridge job has invalid downstream project', :sidekiq_inline do
      let(:config) do
        <<-EOY
        test:
          stage: test
          script: echo test

        deploy:
          stage: deploy
          trigger:
            project: invalid-project
        EOY
      end

      let(:pipeline) do
        Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
      end

      before do
        stub_ci_pipeline_yaml_file(config)
      end

      it 'creates a pipeline, then fails the bridge job' do
        expect(all_builds_names).to contain_exactly('test', 'deploy')
        expect(all_builds_statuses).to contain_exactly('pending', 'created')

        succeed_pending

        expect(all_builds_names).to contain_exactly('test', 'deploy')
        expect(all_builds_statuses).to contain_exactly('success', 'failed')
      end
    end

    context 'when the dependency is stage-independent', :sidekiq_inline do
      let(:config) do
        <<-EOY
        stages: [A, B]

        A1:
          stage: A
          script: exit 0
          when: manual

        A2:
          stage: A
          script: exit 0
          needs: [A1]

        B:
          stage: B
          needs: [A2]
          script: exit 0
        EOY
      end

      let(:pipeline) do
        Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
      end

      before do
        stub_ci_pipeline_yaml_file(config)
      end

      it 'processes subsequent jobs in the correct order when playing first job' do
        expect(all_builds_names).to eq(%w[A1 A2 B])
        expect(all_builds_statuses).to eq(%w[manual skipped skipped])

        play_manual_action('A1')

        expect(all_builds_names).to eq(%w[A1 A2 B])
        expect(all_builds_statuses).to eq(%w[pending created created])
      end
    end

    context 'when the exclusive lease is taken' do
      let(:lease_key) { "ci/pipeline_processing/atomic_processing_service::pipeline_id:#{pipeline.id}" }

      it 'skips pipeline processing' do
        create_build('linux', stage_idx: 0)

        stub_exclusive_lease_taken(lease_key)

        expect(Gitlab::AppJsonLogger).to receive(:info).with(a_hash_including(message: /^Cannot obtain an exclusive lease/))
        expect(process_pipeline).to be_falsy
      end
    end

    describe 'deployments creation' do
      let(:config) do
        <<-YAML
        stages: [stage-0, stage-1, stage-2, stage-3, stage-4]

        test:
          stage: stage-0
          script: exit 0

        review:
          stage: stage-1
          environment:
            name: review
            action: start
          script: exit 0

        staging:
          stage: stage-2
          environment:
            name: staging
            action: start
          script: exit 0
          when: manual
          allow_failure: false

        canary:
          stage: stage-3
          environment:
            name: canary
            action: start
          script: exit 0
          when: manual

        production-a:
          stage: stage-4
          environment:
            name: production-a
            action: start
          script: exit 0
          when: manual

        production-b:
          stage: stage-4
          environment:
            name: production-b
            action: start
          script: exit 0
          when: manual
          needs: [canary]
        YAML
      end

      let(:pipeline) do
        Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push).payload
      end

      let(:test_job) { all_builds.find_by(name: 'test') }
      let(:review_deploy_job) { all_builds.find_by(name: 'review') }
      let(:staging_deploy_job) { all_builds.find_by(name: 'staging') }
      let(:canary_deploy_job) { all_builds.find_by(name: 'canary') }
      let(:production_a_deploy_job) { all_builds.find_by(name: 'production-a') }
      let(:production_b_deploy_job) { all_builds.find_by(name: 'production-b') }

      before do
        create(:environment, name: 'review', project: project)
        create(:environment, name: 'staging', project: project)
        create(:environment, name: 'canary', project: project)
        create(:environment, name: 'production-a', project: project)
        create(:environment, name: 'production-b', project: project)

        stub_ci_pipeline_yaml_file(config)
        pipeline # create the pipeline
      end

      it 'creates deployment records for the deploy jobs', :aggregate_failures do
        # processes the 'test' job, not creating a Deployment record
        expect { process_pipeline }.not_to change { Deployment.count }
        succeed_pending
        expect(test_job.status).to eq 'success'

        # processes automatic 'review' deploy job, creating a Deployment record
        expect { process_pipeline }.to change { Deployment.count }.by(1)
        succeed_pending
        expect(review_deploy_job.status).to eq 'success'

        # processes manual 'staging' deploy job, creating a Deployment record
        # the subsequent manual deploy jobs ('canary', 'production-a', 'production-b')
        #   are not yet processed because 'staging' is set as `allow_failure: false`
        expect { process_pipeline }.to change { Deployment.count }.by(1)
        play_manual_action('staging')
        succeed_pending
        expect(staging_deploy_job.reload.status).to eq 'success'

        # processes manual 'canary' deployment job
        # the subsequent manual deploy jobs ('production-a' and 'production-b')
        #   are also processed because 'canary' is set by default as `allow_failure: true`
        #   the 'production-b' is set as `needs: [canary]`, but it is still processed
        # overall, 3 Deployment records are created
        expect { process_pipeline }.to change { Deployment.count }.by(3)
        expect(canary_deploy_job.status).to eq 'manual'
        expect(production_a_deploy_job.status).to eq 'manual'
        expect(production_b_deploy_job.status).to eq 'skipped'

        # play and succeed the manual 'canary' and 'production-a' jobs
        play_manual_action('canary')
        play_manual_action('production-a')
        succeed_pending
        expect(canary_deploy_job.reload.status).to eq 'success'
        expect(production_a_deploy_job.reload.status).to eq 'success'
        expect(production_b_deploy_job.reload.status).to eq 'created'

        # process the manual 'production-b' job again, no Deployment record is created
        #   because it has already been created when 'production-b' was first processed
        expect { process_pipeline }.not_to change { Deployment.count }
        expect(production_b_deploy_job.reload.status).to eq 'manual'
      end
    end

    private

    def all_builds
      pipeline.all_jobs.order(:stage_idx, :id)
    end

    def builds
      all_builds.where.not(status: [:created, :skipped])
    end

    def stages
      pipeline.reset.stages.map(&:status)
    end

    def builds_names
      builds.pluck(:name)
    end

    def builds_names_and_statuses
      builds.each_with_object({}) do |b, h|
        h[b.name.to_sym] = b.status
      end
    end

    def all_builds_names_and_statuses
      all_builds.each_with_object({}) do |b, h|
        h[b.name.to_sym] = b.status
      end
    end

    def all_builds_names
      all_builds.pluck(:name)
    end

    def builds_statuses
      builds.pluck(:status)
    end

    def all_builds_statuses
      all_builds.pluck(:status)
    end

    def succeed_pending
      builds.pending.each do |build|
        build.reset.success
      end
    end

    def succeed_running_or_pending
      pipeline.builds.running_or_pending.each do |build|
        build.reset.success
      end
    end

    def fail_running_or_pending
      pipeline.builds.running_or_pending.each do |build|
        build.reset.drop
      end
    end

    def cancel_running_or_pending
      pipeline.builds.running_or_pending.each do |build|
        build.reset.cancel
      end
    end

    def play_manual_action(name)
      builds.find_by(name: name).play(user)
    end

    def enqueue_scheduled(name)
      builds.scheduled.find_by(name: name).enqueue!
    end

    def retry_build(name)
      Ci::RetryJobService.new(project, user).execute(builds.find_by(name: name))
    end

    def manual_actions
      pipeline.manual_actions.reload
    end

    def create_build(name, **opts)
      create(:ci_build, :created, pipeline: pipeline, name: name, **with_stage_opts(opts))
    end

    def successful_build(name, **opts)
      create(:ci_build, :success, pipeline: pipeline, name: name, **with_stage_opts(opts))
    end

    def with_stage_opts(opts)
      { stage: "stage-#{opts[:stage_idx].to_i}" }.merge(opts)
    end

    def delayed_options
      { when: 'delayed', options: { script: %w[echo], start_in: '1 minute' } }
    end

    def unschedule
      pipeline.builds.scheduled.map(&:unschedule)
    end
  end

  private

  def process_pipeline
    described_class.new(pipeline).execute
  end

  # A status collection is initialized at the start of pipeline processing and then again at the
  # end of processing.  Here we simulate "playing" the given jobs during pipeline processing by
  # stubbing stopped_job_names so that they appear to have been stopped at the beginning of
  # processing and then later changed to alive status at the end.
  def mock_play_jobs_during_processing(jobs)
    collection = Ci::PipelineProcessing::AtomicProcessingService::StatusCollection.new(pipeline)

    allow(collection).to receive(:stopped_job_names).and_return(jobs.map(&:name), [])

    # Return the same collection object for every instance of StatusCollection
    allow(Ci::PipelineProcessing::AtomicProcessingService::StatusCollection).to receive(:new)
      .and_return(collection)
  end
end


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

module Ci
  module PipelineProcessing
    class AtomicProcessingService
      class StatusCollection
        include Gitlab::Utils::StrongMemoize

        attr_reader :pipeline

        def initialize(pipeline)
          @pipeline = pipeline
          @stage_jobs = {}
          @prior_stage_jobs = {}
        end

        # This method updates internal status for given ID
        def set_job_status(id, status, lock_version)
          job = all_jobs_by_id[id]
          return unless job

          job[:status] = status
          job[:lock_version] = lock_version
        end

        # This methods gets composite status of all jobs
        def status_of_all
          status_for_array(all_jobs)
        end

        # This methods gets composite status for jobs at a given stage
        def status_of_stage(stage_position)
          strong_memoize("status_of_stage_#{stage_position}") do
            stage_jobs = all_jobs_grouped_by_stage_position[stage_position].to_a

            status_for_array(stage_jobs.flatten)
          end
        end

        # This methods gets composite status for jobs with given names
        def status_of_jobs(names)
          jobs = all_jobs_by_name.slice(*names)

          status_for_array(jobs.values, dag: true)
        end

        # This methods gets composite status for jobs before given stage
        def status_of_jobs_prior_to_stage(stage_position)
          strong_memoize("status_of_jobs_prior_to_stage_#{stage_position}") do
            stage_jobs = all_jobs_grouped_by_stage_position
              .select { |position, _| position < stage_position }

            status_for_array(stage_jobs.values.flatten)
          end
        end

        # This methods gets a list of jobs for a given stage
        def created_job_ids_in_stage(stage_position)
          all_jobs_grouped_by_stage_position[stage_position]
            .to_a
            .select { |job| job[:status] == 'created' }
            .map { |job| job[:id] }
        end

        # This method returns a list of all job, that are to be processed
        def processing_jobs
          all_jobs.lazy.reject { |job| job[:processed] }
        end

        # This method returns the names of jobs that have a stopped status
        def stopped_job_names
          all_jobs.select { |job| job[:status].in?(Ci::HasStatus::STOPPED_STATUSES) }.pluck(:name) # rubocop: disable CodeReuse/ActiveRecord
        end

        private

        # We use these columns to perform an efficient calculation of a status
        JOB_ATTRS = [
          :id, :name, :status, :allow_failure,
          :stage_idx, :processed, :lock_version
        ].freeze

        def status_for_array(jobs, dag: false)
          result = Gitlab::Ci::Status::Composite
            .new(jobs, dag: dag, project: pipeline.project)
            .status
          result || 'success'
        end

        def all_jobs_grouped_by_stage_position
          strong_memoize(:all_jobs_by_order) do
            all_jobs.group_by { |job| job[:stage_idx].to_i }
          end
        end

        def all_jobs_by_id
          strong_memoize(:all_jobs_by_id) do
            all_jobs.index_by { |row| row[:id] }
          end
        end

        def all_jobs_by_name
          strong_memoize(:jobs_by_name) do
            all_jobs.index_by { |row| row[:name] }
          end
        end

        # rubocop: disable CodeReuse/ActiveRecord
        def all_jobs
          # We fetch all relevant data in one go.
          #
          # This is more efficient than relying on PostgreSQL to calculate composite status for us
          #
          # Since we need to reprocess everything we can fetch all of them and do processing ourselves.
          strong_memoize(:all_jobs) do
            raw_jobs = pipeline
              .current_jobs
              .ordered_by_stage
              .pluck(*JOB_ATTRS)

            raw_jobs.map do |row|
              JOB_ATTRS.zip(row).to_h
            end
          end
        end
        # rubocop: enable CodeReuse/ActiveRecord
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection,
  feature_category: :continuous_integration do
  using RSpec::Parameterized::TableSyntax

  let_it_be(:pipeline) { create(:ci_pipeline) }
  let_it_be(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
  let_it_be(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
  let_it_be(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
  let_it_be(:build_a) do
    create(:ci_build, :success, name: 'build-a', ci_stage: build_stage, stage_idx: 0, pipeline: pipeline)
  end

  let_it_be(:build_b) do
    create(:ci_build, :failed, name: 'build-b', ci_stage: build_stage, stage_idx: 0, pipeline: pipeline)
  end

  let_it_be(:test_a) do
    create(:ci_build, :running, name: 'test-a', ci_stage: test_stage, stage_idx: 1, pipeline: pipeline)
  end

  let_it_be(:test_b) do
    create(:ci_build, :pending, name: 'test-b', ci_stage: test_stage, stage_idx: 1, pipeline: pipeline)
  end

  let_it_be(:deploy) do
    create(:ci_build, :created, name: 'deploy', ci_stage: deploy_stage, stage_idx: 2, pipeline: pipeline)
  end

  let(:collection) { described_class.new(pipeline) }

  describe '#set_job_status' do
    it 'does update existing status of job' do
      collection.set_job_status(test_a.id, 'success', 100)

      expect(collection.status_of_jobs(['test-a'])).to eq('success')
    end

    it 'ignores a missing job' do
      collection.set_job_status(-1, 'failed', 100)
    end
  end

  describe '#status_of_all' do
    it 'returns composite status of the collection' do
      expect(collection.status_of_all).to eq('running')
    end
  end

  describe '#status_of_jobs' do
    where(:names, :status) do
      %w[build-a]         | 'success'
      %w[build-a build-b] | 'failed'
      %w[build-a test-a]  | 'running'
    end

    with_them do
      it 'returns composite status of given names' do
        expect(collection.status_of_jobs(names)).to eq(status)
      end
    end
  end

  describe '#status_of_jobs_prior_to_stage' do
    where(:stage, :status) do
      0 | 'success'
      1 | 'failed'
      2 | 'running'
    end

    with_them do
      it 'returns composite status for jobs in prior stages' do
        expect(collection.status_of_jobs_prior_to_stage(stage)).to eq(status)
      end
    end
  end

  describe '#status_of_stage' do
    where(:stage, :status) do
      0 | 'failed'
      1 | 'running'
      2 | 'created'
    end

    with_them do
      it 'returns composite status for jobs at a given stages' do
        expect(collection.status_of_stage(stage)).to eq(status)
      end
    end
  end

  describe '#created_job_ids_in_stage' do
    it 'returns IDs of jobs at a given stage position' do
      expect(collection.created_job_ids_in_stage(0)).to be_empty
      expect(collection.created_job_ids_in_stage(1)).to be_empty
      expect(collection.created_job_ids_in_stage(2)).to contain_exactly(deploy.id)
    end
  end

  describe '#processing_jobs' do
    it 'returns jobs marked as processing' do
      expect(collection.processing_jobs.map { |job| job[:id] })
        .to contain_exactly(build_a.id, build_b.id, test_a.id, test_b.id, deploy.id)
    end
  end

  describe '#stopped_job_names' do
    it 'returns names of jobs that have a stopped status' do
      expect(collection.stopped_job_names)
        .to contain_exactly(build_a.name, build_b.name)
    end
  end
end


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

module Ci
  module Catalog
    module Resources
      class ValidateService
        MINIMUM_AMOUNT_OF_COMPONENTS = 1

        def initialize(project, ref)
          @project = project
          @ref = ref
          @errors = []
        end

        def execute
          verify_presence_project_readme
          verify_presence_project_description
          scan_directory_for_components

          if errors.empty?
            ServiceResponse.success
          else
            ServiceResponse.error(message: errors.join(', '))
          end
        end

        private

        attr_reader :project, :ref, :errors

        def verify_presence_project_readme
          return if project_has_readme?

          errors << 'Project must have a README'
        end

        def verify_presence_project_description
          return if project.description.present?

          errors << 'Project must have a description'
        end

        def scan_directory_for_components
          return if Ci::Catalog::ComponentsProject.new(project).fetch_component_paths(ref,
            limit: MINIMUM_AMOUNT_OF_COMPONENTS).any?

          errors << 'Project must contain components. Ensure you are using the correct directory structure'
        end

        def project_has_readme?
          project.repository.blob_data_at(ref, 'README.md')
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Catalog::Resources::ValidateService, feature_category: :pipeline_composition do
  describe '#execute' do
    context 'when a project has a README, a description, and at least one component' do
      it 'is valid' do
        project = create(:project, :catalog_resource_with_components)
        response = described_class.new(project, project.default_branch).execute

        expect(response).to be_success
      end
    end

    context 'when a project has neither a description nor a README nor components' do
      it 'is not valid' do
        project = create(:project, :small_repo)
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq(
          'Project must have a README, ' \
          'Project must have a description, ' \
          'Project must contain components. Ensure you are using the correct directory structure')
      end
    end

    context 'when a project has components but has neither a description nor a README' do
      it 'is not valid' do
        project = create(:project, :small_repo, files: { 'templates/dast/template.yml' => 'image: alpine' })
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq('Project must have a README, Project must have a description')
      end
    end

    context 'when a project has a description but has neither a README nor components' do
      it 'is not valid' do
        project = create(:project, :small_repo, description: 'project with no README and no components')
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq(
          'Project must have a README, ' \
          'Project must contain components. Ensure you are using the correct directory structure')
      end
    end

    context 'when a project has a README but has neither a description nor components' do
      it 'is not valid' do
        project = create(:project, :repository)
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq(
          'Project must have a description, ' \
          'Project must contain components. Ensure you are using the correct directory structure')
      end
    end

    context 'when a project has components and a description but no README' do
      it 'is not valid' do
        project = create(:project, :small_repo, description: 'desc', files: { 'templates/dast.yml' => 'image: alpine' })
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq('Project must have a README')
      end
    end

    context 'when a project has components and a README but no description' do
      it 'is not valid' do
        project = create(:project, :custom_repo,
          files: { 'templates/dast.yml' => 'image: alpine', 'README.md' => 'readme' })
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq('Project must have a description')
      end
    end

    context 'when a project has a description and a README but no components' do
      it 'is not valid' do
        project = create(:project, :readme, description: 'project with no README and no components')
        response = described_class.new(project, project.default_branch).execute

        expect(response.message).to eq(
          'Project must contain components. Ensure you are using the correct directory structure')
      end
    end
  end
end


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

module Ci
  module Catalog
    module Resources
      class ReleaseService
        def initialize(release)
          @release = release
          @project = release.project
          @errors = []
        end

        def execute
          validate_catalog_resource
          create_version

          if errors.empty?
            ServiceResponse.success
          else
            ServiceResponse.error(message: errors.join(', '))
          end
        end

        private

        attr_reader :project, :errors, :release

        def validate_catalog_resource
          response = Ci::Catalog::Resources::ValidateService.new(project, release.sha).execute
          return if response.success?

          errors << response.message
        end

        def create_version
          return if errors.present?

          response = Ci::Catalog::Resources::Versions::CreateService.new(release).execute
          return if response.success?

          errors << response.message
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeline_composition do
  describe '#execute' do
    context 'with a valid catalog resource and release' do
      it 'validates the catalog resource and creates a version' do
        project = create(:project, :catalog_resource_with_components)
        catalog_resource = create(:ci_catalog_resource, project: project)
        release = create(:release, project: project, sha: project.repository.root_ref_sha)

        response = described_class.new(release).execute

        version = Ci::Catalog::Resources::Version.last

        expect(response).to be_success
        expect(version.release).to eq(release)
        expect(version.catalog_resource).to eq(catalog_resource)
        expect(version.catalog_resource.project).to eq(project)
      end
    end

    context 'when the validation of the catalog resource fails' do
      it 'returns an error and does not create a version' do
        project = create(:project, :repository)
        create(:ci_catalog_resource, project: project)
        release = create(:release, project: project, sha: project.repository.root_ref_sha)

        response = described_class.new(release).execute

        expect(Ci::Catalog::Resources::Version.count).to be(0)
        expect(response).to be_error
        expect(response.message).to eq(
          'Project must have a description, ' \
          'Project must contain components. Ensure you are using the correct directory structure')
      end
    end

    context 'when the creation of a version fails' do
      it 'returns an error and does not create a version' do
        project =
          create(
            :project, :custom_repo,
            description: 'Component project',
            files: {
              'templates/secret-detection.yml' => 'image: agent: coop',
              'README.md' => 'Read me'
            }
          )
        create(:ci_catalog_resource, project: project)
        release = create(:release, project: project, sha: project.repository.root_ref_sha)

        response = described_class.new(release).execute

        expect(Ci::Catalog::Resources::Version.count).to be(0)
        expect(response).to be_error
        expect(response.message).to include('mapping values are not allowed in this context')
      end
    end
  end
end


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

module Ci
  module Catalog
    module Resources
      class DestroyService
        include Gitlab::Allowable

        attr_reader :project, :current_user

        def initialize(project, user)
          @current_user = user
          @project = project
        end

        def execute(catalog_resource)
          raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource,
            project)

          catalog_resource.destroy!

          ServiceResponse.success(message: 'Catalog Resource destroyed')
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Catalog::Resources::DestroyService, feature_category: :pipeline_composition do
  let_it_be(:project) { create(:project, :catalog_resource_with_components) }
  let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) }
  let_it_be(:user) { create(:user) }

  let(:service) { described_class.new(project, user) }

  before do
    stub_licensed_features(ci_namespace_catalog: true)
  end

  describe '#execute' do
    context 'with an unauthorized user' do
      it 'raises an AccessDeniedError' do
        expect { service.execute(catalog_resource) }.to raise_error(Gitlab::Access::AccessDeniedError)
      end
    end

    context 'with an authorized user' do
      before_all do
        project.add_owner(user)
      end

      it 'destroys a catalog resource' do
        expect(project.catalog_resource).to eq(catalog_resource)

        response = service.execute(catalog_resource)

        expect(project.reload.catalog_resource).to be_nil
        expect(response.status).to be(:success)
      end
    end
  end
end


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

module Ci
  module Catalog
    module Resources
      class CreateService
        include Gitlab::Allowable

        attr_reader :project, :current_user

        def initialize(project, user)
          @current_user = user
          @project = project
        end

        def execute
          raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)

          catalog_resource = Ci::Catalog::Resource.new(project: project)

          if catalog_resource.valid?
            catalog_resource.save!
            ServiceResponse.success(payload: catalog_resource)
          else
            ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
          end
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipeline_composition do
  let_it_be(:project) { create(:project, :catalog_resource_with_components) }
  let_it_be(:user) { create(:user) }

  let(:service) { described_class.new(project, user) }

  before do
    stub_licensed_features(ci_namespace_catalog: true)
  end

  describe '#execute' do
    context 'with an unauthorized user' do
      it 'raises an AccessDeniedError' do
        expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
      end
    end

    context 'with an authorized user' do
      before_all do
        project.add_owner(user)
      end

      context 'and a valid project' do
        it 'creates a catalog resource' do
          response = service.execute

          expect(response.payload.project).to eq(project)
        end
      end

      context 'with an invalid catalog resource' do
        it 'does not save the catalog resource' do
          catalog_resource = instance_double(::Ci::Catalog::Resource,
            valid?: false,
            errors: instance_double(ActiveModel::Errors, full_messages: ['not valid']))
          allow(::Ci::Catalog::Resource).to receive(:new).and_return(catalog_resource)

          response = service.execute

          expect(response.message).to eq('not valid')
        end
      end
    end
  end
end


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

module Ci
  module Catalog
    module Resources
      module Versions
        class CreateService
          def initialize(release)
            @project = release.project
            @release = release
            @errors = []
            @version = nil
            @components_project = Ci::Catalog::ComponentsProject.new(project)
          end

          def execute
            build_catalog_resource_version
            fetch_and_build_components if Feature.enabled?(:ci_catalog_create_metadata, project)
            publish_catalog_resource!

            if errors.empty?
              ServiceResponse.success
            else
              ServiceResponse.error(message: errors.flatten.first(10).join(', '))
            end
          end

          private

          attr_reader :project, :errors, :release, :components_project

          def build_catalog_resource_version
            return error('Project is not a catalog resource') unless project.catalog_resource

            @version = Ci::Catalog::Resources::Version.new(
              release: release,
              catalog_resource: project.catalog_resource,
              project: project
            )
          end

          def fetch_and_build_components
            return if errors.present?

            max_components = Ci::Catalog::ComponentsProject::COMPONENTS_LIMIT
            component_paths = components_project.fetch_component_paths(release.sha, limit: max_components + 1)

            if component_paths.size > max_components
              return error("Release cannot contain more than #{max_components} components")
            end

            build_components(component_paths)
          end

          def build_components(component_paths)
            paths_with_oids = component_paths.map { |path| [release.sha, path] }
            blobs = project.repository.blobs_at(paths_with_oids)

            blobs.each do |blob|
              metadata = extract_metadata(blob)
              build_catalog_resource_component(metadata)
            end
          rescue ::Gitlab::Config::Loader::FormatError => e
            error(e)
          end

          def extract_metadata(blob)
            {
              name: components_project.extract_component_name(blob.path),
              inputs: components_project.extract_inputs(blob.data),
              path: blob.path
            }
          end

          def build_catalog_resource_component(metadata)
            return if errors.present?

            component = @version.components.build(
              name: metadata[:name],
              project: @version.project,
              inputs: metadata[:inputs],
              catalog_resource: @version.catalog_resource,
              path: metadata[:path],
              created_at: Time.current
            )

            return if component.valid?

            error("Build component error: #{component.errors.full_messages.join(', ')}")
          end

          def publish_catalog_resource!
            return if errors.present?

            ::Ci::Catalog::Resources::Version.transaction do
              BulkInsertableAssociations.with_bulk_insert do
                @version.save!
              end

              project.catalog_resource.publish!
            end
          end

          def error(message)
            errors << message
          end
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category: :pipeline_composition do
  describe '#execute' do
    let(:files) do
      {
        'templates/secret-detection.yml' => "spec:\n inputs:\n  website:\n---\nimage: alpine_1",
        'templates/dast/template.yml' => 'image: alpine_2',
        'templates/blank-yaml.yml' => '',
        'templates/dast/sub-folder/template.yml' => 'image: alpine_3',
        'templates/template.yml' => "spec:\n inputs:\n  environment:\n---\nimage: alpine_6",
        'tests/test.yml' => 'image: alpine_7',
        'README.md' => 'Read me'
      }
    end

    let(:project) do
      create(
        :project, :custom_repo,
        description: 'Simple and Complex components',
        files: files
      )
    end

    let(:release) { create(:release, project: project, sha: project.repository.root_ref_sha) }
    let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }

    context 'when the project is not a catalog resource' do
      it 'does not create a version' do
        project = create(:project, :repository)
        release =  create(:release, project: project, sha: project.repository.root_ref_sha)

        response = described_class.new(release).execute

        expect(response).to be_error
        expect(response.message).to include('Project is not a catalog resource')
      end
    end

    context 'when the catalog resource has different types of components and a release' do
      it 'creates a version for the release' do
        response = described_class.new(release).execute

        expect(response).to be_success

        version = Ci::Catalog::Resources::Version.last

        expect(version.release).to eq(release)
        expect(version.catalog_resource).to eq(catalog_resource)
        expect(version.catalog_resource.project).to eq(project)
      end

      it 'marks the catalog resource as published' do
        described_class.new(release).execute

        expect(catalog_resource.reload.state).to eq('published')
      end

      context 'when the ci_catalog_create_metadata feature flag is disabled' do
        before do
          stub_feature_flags(ci_catalog_create_metadata: false)
        end

        it 'does not create components' do
          expect(Ci::Catalog::Resources::Component).not_to receive(:bulk_insert!).and_call_original
          expect(project.ci_components.count).to eq(0)

          response = described_class.new(release).execute

          expect(response).to be_success
          expect(project.ci_components.count).to eq(0)
        end
      end

      context 'when the ci_catalog_create_metadata feature flag is enabled' do
        context 'when there are more than 10 components' do
          let(:files) do
            {
              'templates/secret11.yml' => '',
              'templates/secret10.yml' => '',
              'templates/secret8.yml' => '',
              'templates/secret7.yml' => '',
              'templates/secret6.yml' => '',
              'templates/secret5.yml' => '',
              'templates/secret4.yml' => '',
              'templates/secret3.yml' => '',
              'templates/secret2.yml' => '',
              'templates/secret1.yml' => '',
              'templates/secret0.yml' => '',
              'README.md' => 'Read me'
            }
          end

          it 'does not create components' do
            response = described_class.new(release).execute

            expect(response).to be_error
            expect(response.message).to include('Release cannot contain more than 10 components')
            expect(project.ci_components.count).to eq(0)
          end
        end

        it 'bulk inserts all the components' do
          expect(Ci::Catalog::Resources::Component).to receive(:bulk_insert!).and_call_original

          described_class.new(release).execute
        end

        it 'creates components for the catalog resource' do
          expect(project.ci_components.count).to eq(0)
          response = described_class.new(release).execute

          expect(response).to be_success

          version = Ci::Catalog::Resources::Version.last

          expect(project.ci_components.count).to eq(4)
          expect(project.ci_components.first.name).to eq('blank-yaml')
          expect(project.ci_components.first.project).to eq(version.project)
          expect(project.ci_components.first.inputs).to eq({})
          expect(project.ci_components.first.catalog_resource).to eq(version.catalog_resource)
          expect(project.ci_components.first.version).to eq(version)
          expect(project.ci_components.first.path).to eq('templates/blank-yaml.yml')
          expect(project.ci_components.second.name).to eq('dast')
          expect(project.ci_components.second.project).to eq(version.project)
          expect(project.ci_components.second.inputs).to eq({})
          expect(project.ci_components.second.catalog_resource).to eq(version.catalog_resource)
          expect(project.ci_components.second.version).to eq(version)
          expect(project.ci_components.second.path).to eq('templates/dast/template.yml')
          expect(project.ci_components.third.name).to eq('secret-detection')
          expect(project.ci_components.third.project).to eq(version.project)
          expect(project.ci_components.third.inputs).to eq({ "website" => nil })
          expect(project.ci_components.third.catalog_resource).to eq(version.catalog_resource)
          expect(project.ci_components.third.version).to eq(version)
          expect(project.ci_components.third.path).to eq('templates/secret-detection.yml')
          expect(project.ci_components.fourth.name).to eq('template')
          expect(project.ci_components.fourth.project).to eq(version.project)
          expect(project.ci_components.fourth.inputs).to eq({ "environment" => nil })
          expect(project.ci_components.fourth.catalog_resource).to eq(version.catalog_resource)
          expect(project.ci_components.fourth.version).to eq(version)
          expect(project.ci_components.fourth.path).to eq('templates/template.yml')
        end
      end
    end

    context 'with invalid data' do
      let_it_be(:files) do
        {
          'templates/secret-detection.yml' => 'some: invalid: syntax',
          'README.md' => 'Read me'
        }
      end

      it 'returns an error' do
        response = described_class.new(release).execute

        expect(response).to be_error
        expect(response.message).to include('mapping values are not allowed in this context')
      end
    end

    context 'when one or more components are invalid' do
      let_it_be(:files) do
        {
          'templates/secret-detection.yml' => "spec:\n inputs:\n  - website\n---\nimage: alpine_1",
          'README.md' => 'Read me'
        }
      end

      it 'returns an error' do
        response = described_class.new(release).execute

        expect(response).to be_error
        expect(response.message).to include('Inputs must be a valid json schema')
      end
    end
  end
end