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

module BatchedGitRefUpdates
  class ProjectCleanupService
    include ::Gitlab::ExclusiveLeaseHelpers

    LOCK_TIMEOUT = 10.minutes
    GITALY_BATCH_SIZE = 100
    QUERY_BATCH_SIZE = 1000
    MAX_DELETES = 10_000

    def initialize(project_id)
      @project_id = project_id
    end

    def execute
      total_deletes = 0

      in_lock("#{self.class}/#{@project_id}", retries: 0, ttl: LOCK_TIMEOUT) do
        project = Project.find_by_id(@project_id)
        break unless project

        Deletion
          .status_pending
          .for_project(@project_id)
          .select_ref_and_identity
          .each_batch(of: QUERY_BATCH_SIZE) do |batch|
          refs = batch.map(&:ref)

          refs.each_slice(GITALY_BATCH_SIZE) do |refs_to_delete|
            project.repository.delete_refs(*refs_to_delete.uniq)
          end

          total_deletes += refs.count
          Deletion.mark_records_processed(batch)

          break if total_deletes >= MAX_DELETES
        end
      end

      { total_deletes: total_deletes }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe BatchedGitRefUpdates::ProjectCleanupService, feature_category: :gitaly do
  let(:service) { described_class.new(project1.id) }
  let_it_be(:project1) { create(:project, :repository) }
  let_it_be(:project2) { create(:project, :repository) }
  let!(:project1_ref1) do
    BatchedGitRefUpdates::Deletion.create!(project_id: project1.id, ref: 'refs/test/project1-ref1')
  end

  let!(:project1_ref2) do
    BatchedGitRefUpdates::Deletion.create!(project_id: project1.id, ref: 'refs/test/project1-ref2')
  end

  let!(:project1_ref3) do
    BatchedGitRefUpdates::Deletion.create!(project_id: project1.id, ref: 'refs/test/already-processed',
      status: :processed)
  end

  let!(:project2_ref1) do
    BatchedGitRefUpdates::Deletion.create!(project_id: project2.id, ref: 'refs/test/project2-ref1')
  end

  describe '#execute' do
    before do
      project1.repository.create_ref('HEAD', 'refs/test/ref-to-not-be-deleted')
      project1.repository.create_ref('HEAD', project1_ref1.ref)
      project1.repository.create_ref('HEAD', project1_ref2.ref)
      project1.repository.create_ref('HEAD', 'refs/test/already-processed')
      project2.repository.create_ref('HEAD', project2_ref1.ref)
    end

    it 'deletes the named refs in batches for the given project only' do
      expect(test_refs(project1)).to include(
        'refs/test/ref-to-not-be-deleted',
        'refs/test/already-processed',
        'refs/test/project1-ref1',
        'refs/test/project1-ref1',
        'refs/test/project1-ref2')

      service.execute

      expect(test_refs(project1)).to include(
        'refs/test/already-processed',
        'refs/test/ref-to-not-be-deleted')

      expect(test_refs(project1)).not_to include(
        'refs/test/project1-ref1',
        'refs/test/project1-ref2')

      expect(test_refs(project2)).to include('refs/test/project2-ref1')
    end

    it 'handles duplicates' do
      BatchedGitRefUpdates::Deletion.create!(project_id: project1.id, ref: 'refs/test/some-duplicate')
      BatchedGitRefUpdates::Deletion.create!(project_id: project1.id, ref: 'refs/test/some-duplicate')

      service.execute

      expect(test_refs(project1)).not_to include('refs/test/some-duplicate')
    end

    it 'marks the processed BatchedGitRefUpdates::Deletion as processed' do
      service.execute

      expect(BatchedGitRefUpdates::Deletion.status_pending.map(&:ref)).to contain_exactly('refs/test/project2-ref1')
      expect(BatchedGitRefUpdates::Deletion.status_processed.map(&:ref)).to contain_exactly(
        'refs/test/project1-ref1',
        'refs/test/project1-ref2',
        'refs/test/already-processed')
    end

    it 'returns stats' do
      result = service.execute

      expect(result[:total_deletes]).to eq(2)
    end

    it 'acquires a lock for the given project_id to avoid running duplicate instances' do
      expect(service).to receive(:in_lock) # Mock and don't yield
        .with("#{described_class}/#{project1.id}", retries: 0, ttl: described_class::LOCK_TIMEOUT)

      expect { service.execute }.not_to change { BatchedGitRefUpdates::Deletion.status_pending.count }
    end

    it 'does nothing when the project does not exist' do
      result = described_class.new(non_existing_record_id).execute

      expect(result[:total_deletes]).to eq(0)
    end

    it 'stops after it reaches limit of deleted refs' do
      stub_const("#{described_class}::QUERY_BATCH_SIZE", 1)
      stub_const("#{described_class}::MAX_DELETES", 1)

      result = service.execute

      expect(result[:total_deletes]).to eq(1)
    end

    def test_refs(project)
      project.repository.list_refs(['refs/test/']).map(&:name)
    end
  end
end


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

module Ci
  class ProcessSyncEventsService
    include ExclusiveLeaseGuard

    BATCH_SIZE = 1000

    def initialize(sync_event_class, sync_class)
      @sync_event_class = sync_event_class
      @sync_class = sync_class
      @results = {}
    end

    def execute
      # preventing parallel processing over the same event table
      try_obtain_lease { process_events }

      enqueue_worker_if_there_still_event

      @results
    end

    private

    def process_events
      add_result(estimated_total_events: @sync_event_class.upper_bound_count)

      events = @sync_event_class.preload_synced_relation.first(BATCH_SIZE)

      add_result(consumable_events: events.size)

      return if events.empty?

      processed_events = []

      begin
        events.each do |event|
          @sync_class.sync!(event)

          processed_events << event
        end
      ensure
        add_result(processed_events: processed_events.size)
        @sync_event_class.id_in(processed_events).delete_all
      end
    end

    def enqueue_worker_if_there_still_event
      @sync_event_class.enqueue_worker if @sync_event_class.exists?
    end

    def lease_key
      "#{super}::#{@sync_event_class}"
    end

    def lease_timeout
      1.minute
    end

    def add_result(result)
      @results.merge!(result)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ProcessSyncEventsService, feature_category: :continuous_integration do
  let!(:group) { create(:group) }
  let!(:project1) { create(:project, group: group) }
  let!(:project2) { create(:project, group: group) }
  let!(:parent_group_1) { create(:group) }
  let!(:parent_group_2) { create(:group) }

  subject(:service) { described_class.new(sync_event_class, hierarchy_class) }

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

    context 'for Projects::SyncEvent' do
      let(:sync_event_class) { Projects::SyncEvent }
      let(:hierarchy_class) { ::Ci::ProjectMirror }

      before do
        Projects::SyncEvent.delete_all

        project1.update!(group: parent_group_1)
        project2.update!(group: parent_group_2)
      end

      it { is_expected.to eq(service_results(2, 2, 2)) }

      it 'consumes events' do
        expect { execute }.to change(Projects::SyncEvent, :count).from(2).to(0)

        expect(project1.reload.ci_project_mirror).to have_attributes(
          namespace_id: parent_group_1.id
        )
        expect(project2.reload.ci_project_mirror).to have_attributes(
          namespace_id: parent_group_2.id
        )
      end

      context 'when any event left after processing' do
        before do
          stub_const("#{described_class}::BATCH_SIZE", 1)
        end

        it { is_expected.to eq(service_results(2, 1, 1)) }

        it 'enqueues Projects::ProcessSyncEventsWorker' do
          expect(Projects::ProcessSyncEventsWorker).to receive(:perform_async)

          execute
        end
      end

      context 'when no event left after processing' do
        before do
          stub_const("#{described_class}::BATCH_SIZE", 2)
        end

        it { is_expected.to eq(service_results(2, 2, 2)) }

        it 'does not enqueue Projects::ProcessSyncEventsWorker' do
          expect(Projects::ProcessSyncEventsWorker).not_to receive(:perform_async)

          execute
        end
      end

      context 'when there is no event' do
        before do
          Projects::SyncEvent.delete_all
        end

        it { is_expected.to eq(service_results(0, 0, nil)) }

        it 'does nothing' do
          expect { execute }.not_to change(Projects::SyncEvent, :count)
        end
      end

      context 'when there is non-executed events' do
        before do
          new_project = create(:project)
          sync_event_class.delete_all

          project1.update!(group: parent_group_2)
          new_project.update!(group: parent_group_1)
          project2.update!(group: parent_group_1)

          @new_project_sync_event = new_project.sync_events.last

          allow(sync_event_class).to receive(:preload_synced_relation).and_return(
            sync_event_class.where.not(id: @new_project_sync_event)
          )
        end

        it { is_expected.to eq(service_results(3, 2, 2)) }

        it 'does not delete non-executed events' do
          expect { execute }.to change(Projects::SyncEvent, :count).from(3).to(1)
          expect(@new_project_sync_event.reload).to be_persisted
        end
      end

      private

      def service_results(total, consumable, processed)
        {
          estimated_total_events: total,
          consumable_events: consumable,
          processed_events: processed
        }.compact
      end
    end

    context 'for Namespaces::SyncEvent' do
      let(:sync_event_class) { Namespaces::SyncEvent }
      let(:hierarchy_class) { ::Ci::NamespaceMirror }

      before do
        Namespaces::SyncEvent.delete_all

        # Creates a sync event for group, and the ProjectNamespace of project1 & project2: 3 in total
        group.update!(parent: parent_group_2)
        # Creates a sync event for parent_group2 and all the children: 4 in total
        parent_group_2.update!(parent: parent_group_1)
      end

      shared_examples 'event consuming' do
        it 'consumes events' do
          expect { execute }.to change(Namespaces::SyncEvent, :count).from(7).to(0)

          expect(group.reload.ci_namespace_mirror).to have_attributes(
            traversal_ids: [parent_group_1.id, parent_group_2.id, group.id]
          )
          expect(parent_group_2.reload.ci_namespace_mirror).to have_attributes(
            traversal_ids: [parent_group_1.id, parent_group_2.id]
          )
          expect(project1.reload.project_namespace).to have_attributes(
            traversal_ids: [parent_group_1.id, parent_group_2.id, group.id, project1.project_namespace.id]
          )
          expect(project2.reload.project_namespace).to have_attributes(
            traversal_ids: [parent_group_1.id, parent_group_2.id, group.id, project2.project_namespace.id]
          )
        end
      end

      it_behaves_like 'event consuming'

      it 'enqueues Namespaces::ProcessSyncEventsWorker if any left' do
        stub_const("#{described_class}::BATCH_SIZE", 1)

        expect(Namespaces::ProcessSyncEventsWorker).to receive(:perform_async)

        execute
      end

      it 'does not enqueue Namespaces::ProcessSyncEventsWorker if no left' do
        stub_const("#{described_class}::BATCH_SIZE", 7)

        expect(Namespaces::ProcessSyncEventsWorker).not_to receive(:perform_async)

        execute
      end
    end
  end
end


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

module Ci
  class TestFailureHistoryService
    class Async
      attr_reader :service

      def initialize(service)
        @service = service
      end

      def perform_if_needed
        TestFailureHistoryWorker.perform_async(service.pipeline.id) if service.should_track_failures?
      end
    end

    MAX_TRACKABLE_FAILURES = 200

    attr_reader :pipeline

    delegate :project, to: :pipeline

    def initialize(pipeline)
      @pipeline = pipeline
    end

    def execute
      return unless should_track_failures?

      track_failures
    end

    def should_track_failures?
      return false unless project.default_branch_or_main == pipeline.ref

      # We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get
      # 201 total number of builds with the assumption that each job has at least
      # 1 failed unit test, then we have at least 201 failed unit tests which exceeds
      # the MAX_TRACKABLE_FAILURES of 200. If this is the case, let's early exit so we
      # don't have to parse each JUnit report of each of the 201 builds.
      failed_builds.length <= MAX_TRACKABLE_FAILURES
    end

    def async
      Async.new(self)
    end

    private

    def failed_builds
      @failed_builds ||= pipeline.builds_with_failed_tests(limit: MAX_TRACKABLE_FAILURES + 1)
    end

    def track_failures
      failed_unit_tests = gather_failed_unit_tests_from_reports(failed_builds)

      return if failed_unit_tests.size > MAX_TRACKABLE_FAILURES

      failed_unit_tests.each_slice(100) do |batch|
        Ci::UnitTest.transaction do
          unit_test_attrs = ci_unit_test_attrs(batch)
          ci_unit_tests = Ci::UnitTest.find_or_create_by_batch(project, unit_test_attrs)

          failures = ci_unit_test_failure_attrs(ci_unit_tests, failed_unit_tests)
          Ci::UnitTestFailure.insert_all(failures)
        end
      end
    end

    def gather_failed_unit_tests_from_reports(failed_builds)
      failed_builds.each_with_object({}) do |build, failed_unit_tests|
        test_suite = generate_test_suite!(build)
        test_suite.failed.each do |key, unit_test|
          failed_unit_tests[key] = {
            build: build, # This will be used in ci_unit_test_failure_attrs
            unit_test: unit_test # This will be used in ci_unit_test_attrs
          }
        end
      end
    end

    def generate_test_suite!(build)
      test_report = build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
      test_report.get_suite(build.test_suite_name)
    end

    def ci_unit_test_attrs(batch)
      batch.map do |item|
        unit_test = item.last[:unit_test]

        {
          key_hash: unit_test.key,
          name: unit_test.name,
          suite_name: unit_test.suite_name
        }
      end
    end

    def ci_unit_test_failure_attrs(ci_unit_tests, failed_unit_tests)
      ci_unit_tests.map do |ci_unit_test|
        build = failed_unit_tests[ci_unit_test.key_hash][:build]

        {
          unit_test_id: ci_unit_test.id,
          build_id: build.id,
          failed_at: build.finished_at,
          partition_id: build.partition_id
        }
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project, :repository) }

  let_it_be_with_reload(:pipeline) do
    create(:ci_pipeline, status: :created, project: project, ref: project.default_branch)
  end

  describe '#execute' do
    subject(:execute_service) { described_class.new(pipeline).execute }

    context 'when pipeline has failed builds with test reports' do
      let_it_be(:job) do
        # The test report has 2 unit test failures
        create(:ci_build, :failed, :test_reports, pipeline: pipeline)
      end

      it 'creates unit test failures records' do
        execute_service

        expect(Ci::UnitTest.count).to eq(2)
        expect(Ci::UnitTestFailure.count).to eq(2)
      end

      it 'assigns partition_id to Ci::UnitTestFailure' do
        execute_service

        unit_test_failure_partition_ids = Ci::UnitTestFailure.distinct.pluck(:partition_id)

        expect(unit_test_failure_partition_ids).to match_array([job.partition_id])
      end

      context 'when pipeline is not for the default branch' do
        before do
          pipeline.update_column(:ref, 'new-feature')
        end

        it 'does not persist data' do
          execute_service

          expect(Ci::UnitTest.count).to eq(0)
          expect(Ci::UnitTestFailure.count).to eq(0)
        end
      end

      context 'when test failure data have already been persisted with the same exact attributes' do
        before do
          execute_service
        end

        it 'does not fail but does not persist new data' do
          expect { described_class.new(pipeline).execute }.not_to raise_error

          expect(Ci::UnitTest.count).to eq(2)
          expect(Ci::UnitTestFailure.count).to eq(2)
        end
      end

      context 'when number of failed unit tests exceed the limit' do
        before do
          stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1)
        end

        it 'does not persist data' do
          execute_service

          expect(Ci::UnitTest.count).to eq(0)
          expect(Ci::UnitTestFailure.count).to eq(0)
        end
      end

      context 'when number of failed unit tests across multiple builds exceed the limit' do
        before do
          stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 2)

          # This other test report has 1 unique unit test failure which brings us to 3 total failures across all builds
          # thus exceeding the limit of 2 for MAX_TRACKABLE_FAILURES
          create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline)
        end

        it 'does not persist data' do
          execute_service

          expect(Ci::UnitTest.count).to eq(0)
          expect(Ci::UnitTestFailure.count).to eq(0)
        end
      end
    end

    context 'when test failure data have duplicates within the same payload (happens when the JUnit report has duplicate unit test names but have different failures)' do
      before do
        # The test report has 2 unit test failures but with the same unit test keys
        create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline)
      end

      it 'does not fail but does not persist duplicate data' do
        expect { execute_service }.not_to raise_error

        expect(Ci::UnitTest.count).to eq(1)
        expect(Ci::UnitTestFailure.count).to eq(1)
      end
    end

    context 'when pipeline has no failed builds with test reports' do
      before do
        create(:ci_build, :test_reports, pipeline: pipeline)
        create(:ci_build, :failed, pipeline: pipeline)
      end

      it 'does not persist data' do
        execute_service

        expect(Ci::UnitTest.count).to eq(0)
        expect(Ci::UnitTestFailure.count).to eq(0)
      end
    end
  end

  describe '#should_track_failures?' do
    subject { described_class.new(pipeline).should_track_failures? }

    let_it_be(:jobs) do
      create_list(:ci_build, 2, :test_reports, :failed, pipeline: pipeline)
    end

    context 'when feature flag is enabled and pipeline ref is the default branch' do
      it { is_expected.to eq(true) }
    end

    context 'when pipeline is not equal to the project default branch' do
      before do
        pipeline.update_column(:ref, 'some-other-branch')
      end

      it { is_expected.to eq(false) }
    end

    context 'when total number of builds with failed tests exceeds the max number of trackable failures' do
      before do
        stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1)
      end

      it { is_expected.to eq(false) }
    end
  end

  describe '#async' do
    let(:pipeline) { double(id: 1) }
    let(:service) { described_class.new(pipeline) }

    context 'when service should track failures' do
      before do
        allow(service).to receive(:should_track_failures?).and_return(true)
      end

      it 'enqueues the worker when #perform_if_needed is called' do
        expect(Ci::TestFailureHistoryWorker).to receive(:perform_async).with(pipeline.id)

        service.async.perform_if_needed
      end
    end

    context 'when service should not track failures' do
      before do
        allow(service).to receive(:should_track_failures?).and_return(false)
      end

      it 'does not enqueue the worker when #perform_if_needed is called' do
        expect(Ci::TestFailureHistoryWorker).not_to receive(:perform_async)

        service.async.perform_if_needed
      end
    end
  end
end


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

module Ci
  class UnlockPipelineService
    include BaseServiceUtility
    include ::Gitlab::ExclusiveLeaseHelpers

    ExecutionTimeoutError = Class.new(StandardError)

    BATCH_SIZE = 100
    MAX_EXEC_DURATION = 10.minutes.freeze
    LOCK_TIMEOUT = (MAX_EXEC_DURATION + 1.minute).freeze

    def initialize(pipeline)
      @pipeline = pipeline
      @already_leased = false
      @already_unlocked = false
      @exec_timeout = false
      @unlocked_job_artifacts_count = 0
      @unlocked_pipeline_artifacts_count = 0
    end

    def execute
      unlock_pipeline_exclusively

      success(
        skipped_already_leased: already_leased,
        skipped_already_unlocked: already_unlocked,
        exec_timeout: exec_timeout,
        unlocked_job_artifacts: unlocked_job_artifacts_count,
        unlocked_pipeline_artifacts: unlocked_pipeline_artifacts_count
      )
    end

    private

    attr_reader :pipeline, :already_leased, :already_unlocked, :exec_timeout,
      :unlocked_job_artifacts_count, :unlocked_pipeline_artifacts_count

    def unlock_pipeline_exclusively
      in_lock(lock_key, ttl: LOCK_TIMEOUT, retries: 0) do
        # Even though we enforce uniqueness when enqueueing pipelines, there is still a rare race condition chance that
        # a pipeline can be re-enqueued right after a worker pops off the same pipeline ID from the queue, and then just
        # after it completing the unlock process and releasing the lock, another worker picks up the re-enqueued
        # pipeline ID. So let's make sure to only unlock artifacts if the pipeline has not been unlocked.
        if pipeline.unlocked?
          @already_unlocked = true
          break
        end

        unlock_job_artifacts
        unlock_pipeline_artifacts

        # Marking the row in `ci_pipelines` to unlocked signifies that all artifacts have
        # already been unlocked. This must always happen last.
        unlock_pipeline
      end
    rescue ExecutionTimeoutError
      @exec_timeout = true
    rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
      @already_leased = true
    ensure
      if pipeline.unlocked?
        Ci::UnlockPipelineRequest.log_event(:completed, pipeline.id) unless already_unlocked
      else
        # This is to ensure to re-enqueue the pipeline in 2 occasions:
        # 1. When an unexpected error happens.
        # 2. When the execution timeout has been reached in the case of a pipeline having a lot of
        #    job artifacts. This allows us to continue unlocking the rest of the artifacts from
        #    where we left off. This is why we unlock the pipeline last.
        Ci::UnlockPipelineRequest.enqueue(pipeline.id)
        Ci::UnlockPipelineRequest.log_event(:re_enqueued, pipeline.id)
      end
    end

    def lock_key
      "ci:unlock_pipeline_service:lock:#{pipeline.id}"
    end

    def unlock_pipeline
      pipeline.update_column(:locked, Ci::Pipeline.lockeds[:unlocked])
    end

    def unlock_job_artifacts
      start = Time.current

      pipeline.builds.each_batch(of: BATCH_SIZE) do |builds|
        # rubocop: disable CodeReuse/ActiveRecord
        Ci::JobArtifact.where(job_id: builds.pluck(:id)).each_batch(of: BATCH_SIZE) do |job_artifacts|
          unlocked_count = Ci::JobArtifact
            .where(id: job_artifacts.pluck(:id))
            .update_all(locked: :unlocked)

          @unlocked_job_artifacts_count ||= 0
          @unlocked_job_artifacts_count += unlocked_count

          raise ExecutionTimeoutError if (Time.current - start) > MAX_EXEC_DURATION
        end
        # rubocop: enable CodeReuse/ActiveRecord
      end
    end

    def unlock_pipeline_artifacts
      @unlocked_pipeline_artifacts_count = pipeline.pipeline_artifacts.update_all(locked: :unlocked)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::UnlockPipelineService, :unlock_pipelines, :clean_gitlab_redis_shared_state, feature_category: :build_artifacts do
  describe '#execute', :aggregate_failures do
    let(:service) { described_class.new(pipeline) }

    let!(:pipeline) do
      create(
        :ci_pipeline,
        :with_coverage_report_artifact,
        :with_codequality_mr_diff_report,
        :with_persisted_artifacts,
        locked: :artifacts_locked
      )
    end

    subject(:execute) { service.execute }

    context 'when pipeline is not yet exclusively leased' do
      before do
        stub_const("#{described_class}::BATCH_SIZE", 1)
      end

      it 'unlocks the pipeline and all its artifacts' do
        expect { execute }
          .to change { pipeline.reload.locked }.from('artifacts_locked').to('unlocked')
          .and change { pipeline.reload.job_artifacts.all?(&:artifact_unlocked?) }.to(true)
          .and change { pipeline.reload.pipeline_artifacts.all?(&:artifact_unlocked?) }.to(true)

        expect(execute).to eq(
          status: :success,
          skipped_already_leased: false,
          skipped_already_unlocked: false,
          exec_timeout: false,
          unlocked_job_artifacts: pipeline.job_artifacts.count,
          unlocked_pipeline_artifacts: pipeline.pipeline_artifacts.count
        )
      end

      context 'and pipeline is already unlocked' do
        before do
          described_class.new(pipeline).execute
        end

        it 'skips the pipeline' do
          expect(Ci::JobArtifact).not_to receive(:where)

          expect(execute).to eq(
            status: :success,
            skipped_already_leased: false,
            skipped_already_unlocked: true,
            exec_timeout: false,
            unlocked_job_artifacts: 0,
            unlocked_pipeline_artifacts: 0
          )
        end
      end

      context 'and max execution duration was reached' do
        let!(:first_artifact) { pipeline.job_artifacts.order(:id).first }
        let!(:last_artifact) { pipeline.job_artifacts.order(:id).last }

        before do
          stub_const("#{described_class}::MAX_EXEC_DURATION", 0.seconds)
        end

        it 'keeps the unlocked state of job artifacts already processed and re-enqueues the pipeline' do
          expect { execute }
            .to change { first_artifact.reload.artifact_unlocked? }.to(true)
            .and not_change { last_artifact.reload.artifact_unlocked? }
            .and not_change { pipeline.reload.locked }
            .and not_change { pipeline.reload.pipeline_artifacts.all?(&:artifact_unlocked?) }
            .and change { pipeline_ids_waiting_to_be_unlocked }.from([]).to([pipeline.id])

          expect(execute).to eq(
            status: :success,
            skipped_already_leased: false,
            skipped_already_unlocked: false,
            exec_timeout: true,
            unlocked_job_artifacts: 1,
            unlocked_pipeline_artifacts: 0
          )
        end
      end

      context 'and an error happened' do
        context 'and was raised in the middle batches of job artifacts being unlocked' do
          let!(:first_artifact) { pipeline.job_artifacts.order(:id).first }
          let!(:last_artifact) { pipeline.job_artifacts.order(:id).last }

          before do
            mock_relation = instance_double('Ci::JobArtifact::ActiveRecord_Relation')
            allow(Ci::JobArtifact).to receive(:where).and_call_original
            allow(Ci::JobArtifact).to receive(:where).with(id: [last_artifact.id]).and_return(mock_relation)
            allow(mock_relation).to receive(:update_all).and_raise('An error')
          end

          it 'keeps the unlocked state of job artifacts already processed and re-enqueues the pipeline' do
            expect { execute }
              .to raise_error('An error')
              .and change { first_artifact.reload.artifact_unlocked? }.to(true)
              .and not_change { last_artifact.reload.artifact_unlocked? }
              .and not_change { pipeline.reload.locked }
              .and not_change { pipeline.reload.pipeline_artifacts.all?(&:artifact_unlocked?) }
              .and change { pipeline_ids_waiting_to_be_unlocked }.from([]).to([pipeline.id])
          end
        end

        context 'and was raised while unlocking pipeline artifacts' do
          before do
            allow(pipeline).to receive_message_chain(:pipeline_artifacts, :update_all).and_raise('An error')
          end

          it 'keeps the unlocked state of job artifacts and re-enqueues the pipeline' do
            expect { execute }
              .to raise_error('An error')
              .and change { pipeline.reload.job_artifacts.all?(&:artifact_unlocked?) }.to(true)
              .and not_change { Ci::PipelineArtifact.where(pipeline_id: pipeline.id).all?(&:artifact_unlocked?) }
              .and not_change { pipeline.reload.locked }.from('artifacts_locked')
              .and change { pipeline_ids_waiting_to_be_unlocked }.from([]).to([pipeline.id])
          end
        end

        context 'and was raised while unlocking pipeline' do
          before do
            allow(pipeline).to receive(:update_column).and_raise('An error')
          end

          it 'keeps the unlocked state of job artifacts and pipeline artifacts and re-enqueues the pipeline' do
            expect { execute }
              .to raise_error('An error')
              .and change { pipeline.reload.job_artifacts.all?(&:artifact_unlocked?) }.to(true)
              .and change { pipeline.reload.pipeline_artifacts.all?(&:artifact_unlocked?) }.to(true)
              .and not_change { pipeline.reload.locked }.from('artifacts_locked')
              .and change { pipeline_ids_waiting_to_be_unlocked }.from([]).to([pipeline.id])
          end
        end
      end
    end

    context 'when pipeline is already exclusively leased' do
      before do
        allow(service).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
      end

      it 'does nothing and returns success' do
        expect { execute }.not_to change { pipeline.reload.locked }

        expect(execute).to include(
          status: :success,
          skipped_already_leased: true,
          unlocked_job_artifacts: 0,
          unlocked_pipeline_artifacts: 0
        )
      end
    end
  end
end


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

module Ci
  class ExpirePipelineCacheService
    class UrlHelpers
      include ::Gitlab::Routing
      include ::GitlabRoutingHelper
    end

    def execute(pipeline, delete: false)
      store = Gitlab::EtagCaching::Store.new

      update_etag_cache(pipeline, store)

      if delete
        Gitlab::Cache::Ci::ProjectPipelineStatus.new(pipeline.project).delete_from_cache
      else
        Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
      end
    end

    private

    def project_pipelines_path(project)
      url_helpers.project_pipelines_path(project, format: :json)
    end

    def project_pipeline_path(project, pipeline)
      url_helpers.project_pipeline_path(project, pipeline, format: :json)
    end

    def commit_pipelines_path(project, commit)
      url_helpers.pipelines_project_commit_path(project, commit.id, format: :json)
    end

    def new_merge_request_pipelines_path(project)
      url_helpers.project_new_merge_request_path(project, format: :json)
    end

    def pipelines_project_merge_request_path(merge_request)
      url_helpers.pipelines_project_merge_request_path(merge_request.target_project, merge_request, format: :json)
    end

    def merge_request_widget_path(merge_request)
      url_helpers.cached_widget_project_json_merge_request_path(merge_request.project, merge_request, format: :json)
    end

    def each_pipelines_merge_request_path(pipeline)
      pipeline.all_merge_requests.each do |merge_request|
        yield(pipelines_project_merge_request_path(merge_request))
        yield(merge_request_widget_path(merge_request))
      end
    end

    def graphql_pipeline_path(pipeline)
      url_helpers.graphql_etag_pipeline_path(pipeline)
    end

    def graphql_pipeline_sha_path(sha)
      url_helpers.graphql_etag_pipeline_sha_path(sha)
    end

    def graphql_project_on_demand_scan_counts_path(project)
      url_helpers.graphql_etag_project_on_demand_scan_counts_path(project)
    end

    # Updates ETag caches of a pipeline.
    #
    # This logic resides in a separate method so that EE can more easily extend
    # it.
    #
    # @param [Ci::Pipeline] pipeline
    # @param [Gitlab::EtagCaching::Store] store
    def update_etag_cache(pipeline, store)
      project = pipeline.project

      etag_paths = [
        project_pipelines_path(project),
        new_merge_request_pipelines_path(project),
        graphql_project_on_demand_scan_counts_path(project)
      ]

      etag_paths << commit_pipelines_path(project, pipeline.commit) unless pipeline.commit.nil?

      each_pipelines_merge_request_path(pipeline) do |path|
        etag_paths << path
      end

      pipeline.upstream_and_all_downstreams.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord
        etag_paths << project_pipeline_path(relative_pipeline.project, relative_pipeline)
        etag_paths << graphql_pipeline_path(relative_pipeline)
        etag_paths << graphql_pipeline_sha_path(relative_pipeline.sha)
      end

      store.touch(*etag_paths)
    end

    def url_helpers
      @url_helpers ||= UrlHelpers.new
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ExpirePipelineCacheService, feature_category: :continuous_integration do
  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  subject { described_class.new }

  describe '#execute' do
    it 'invalidates Etag caching for project pipelines path' do
      pipelines_path = "/#{project.full_path}/-/pipelines.json"
      new_mr_pipelines_path = "/#{project.full_path}/-/merge_requests/new.json"
      pipeline_path = "/#{project.full_path}/-/pipelines/#{pipeline.id}.json"
      graphql_pipeline_path = "/api/graphql:pipelines/id/#{pipeline.id}"
      graphql_pipeline_sha_path = "/api/graphql:pipelines/sha/#{pipeline.sha}"
      graphql_project_on_demand_scan_counts_path = "/api/graphql:on_demand_scan/counts/#{project.full_path}"

      expect_touched_etag_caching_paths(
        pipelines_path,
        new_mr_pipelines_path,
        pipeline_path,
        graphql_pipeline_path,
        graphql_pipeline_sha_path,
        graphql_project_on_demand_scan_counts_path
      )

      subject.execute(pipeline)
    end

    it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do
      merge_request = create(:merge_request, :with_detached_merge_request_pipeline)
      project = merge_request.target_project

      merge_request_pipelines_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/pipelines.json"
      merge_request_widget_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/cached_widget.json"

      expect_touched_etag_caching_paths(
        merge_request_pipelines_path,
        merge_request_widget_path
      )

      subject.execute(merge_request.all_pipelines.last)
    end

    it 'updates the cached status for a project' do
      expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).with(pipeline)

      subject.execute(pipeline)
    end

    context 'destroyed pipeline' do
      let(:project_with_repo) { create(:project, :repository) }
      let!(:pipeline_with_commit) { create(:ci_pipeline, :success, project: project_with_repo, sha: project_with_repo.commit.id) }

      it 'clears the cache', :use_clean_rails_redis_caching do
        create(:commit_status, :success, pipeline: pipeline_with_commit, ref: pipeline_with_commit.ref)

        # Sanity check
        expect(project_with_repo.pipeline_status.has_status?).to be_truthy

        subject.execute(pipeline_with_commit, delete: true)

        pipeline_with_commit.destroy!

        # We need to reset lazy_latest_pipeline cache to simulate a new request
        BatchLoader::Executor.clear_current

        # Need to use find to avoid memoization
        expect(Project.find(project_with_repo.id).pipeline_status.has_status?).to be_falsey
      end
    end

    context 'when the pipeline is triggered by another pipeline' do
      let(:source) { create(:ci_sources_pipeline, pipeline: pipeline) }

      it 'updates the cache of dependent pipeline' do
        dependent_pipeline_path = "/#{source.source_project.full_path}/-/pipelines/#{source.source_pipeline.id}.json"

        expect_touched_etag_caching_paths(dependent_pipeline_path)

        subject.execute(pipeline)
      end
    end

    context 'when the pipeline triggered another pipeline' do
      let(:build) { create(:ci_build, pipeline: pipeline) }
      let(:source) { create(:ci_sources_pipeline, source_job: build) }

      it 'updates the cache of dependent pipeline' do
        dependent_pipeline_path = "/#{source.project.full_path}/-/pipelines/#{source.pipeline.id}.json"

        expect_touched_etag_caching_paths(dependent_pipeline_path)

        subject.execute(pipeline)
      end
    end

    it 'does not do N+1 queries' do
      subject.execute(pipeline)

      control = ActiveRecord::QueryRecorder.new { subject.execute(pipeline) }

      create(:ci_sources_pipeline, pipeline: pipeline)
      create(:ci_sources_pipeline, source_job: create(:ci_build, pipeline: pipeline))

      expect { subject.execute(pipeline) }.not_to exceed_query_limit(control.count)
    end
  end

  def expect_touched_etag_caching_paths(*paths)
    expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
      expect(store).to receive(:touch).and_wrap_original do |m, *args|
        expect(args).to include(*paths)

        m.call(*args)
      end
    end
  end
end


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

# This service tracks failed CI builds using Snowplow.
#
# @param build [Ci::Build] the build that failed.
# @param exit_code [Int] the resulting exit code.
module Ci
  class TrackFailedBuildService
    SCHEMA_URL = 'iglu:com.gitlab/ci_build_failed/jsonschema/1-0-2'

    def initialize(build:, exit_code:, failure_reason:)
      @build = build
      @exit_code = exit_code
      @failure_reason = failure_reason
    end

    def execute
      # rubocop:disable Style/IfUnlessModifier
      unless @build.failed?
        return ServiceResponse.error(message: 'Attempted to track a non-failed CI build')
      end

      # rubocop:enable Style/IfUnlessModifier

      context = SnowplowTracker::SelfDescribingJson.new(SCHEMA_URL, payload)

      ::Gitlab::Tracking.event(
        'ci::build',
        'failed',
        context: [context],
        user: @build.user,
        project: @build.project_id)

      ServiceResponse.success
    end

    private

    def payload
      {
        build_id: @build.id,
        build_name: @build.name,
        build_artifact_types: @build.job_artifact_types,
        exit_code: @exit_code,
        failure_reason: @failure_reason,
        project: @build.project_id
      }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::TrackFailedBuildService, feature_category: :continuous_integration do
  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project, :public) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }

  let_it_be(:exit_code) { 42 }
  let_it_be(:failure_reason) { "script_failure" }

  describe '#execute' do
    context 'when a build has failed' do
      let_it_be(:build) { create(:ci_build, :failed, :sast_report, pipeline: pipeline, user: user) }

      subject { described_class.new(build: build, exit_code: exit_code, failure_reason: failure_reason) }

      it 'tracks the build failed event', :snowplow do
        response = subject.execute

        expect(response.success?).to be true

        context = {
          schema: described_class::SCHEMA_URL,
          data: {
            build_id: build.id,
            build_name: build.name,
            build_artifact_types: ["sast"],
            exit_code: exit_code,
            failure_reason: failure_reason,
            project: project.id
          }
        }

        expect_snowplow_event(
          category: 'ci::build',
          action: 'failed',
          context: [context],
          user: user,
          project: project.id)
      end
    end

    context 'when a build has not failed' do
      let_it_be(:build) { create(:ci_build, :success, :sast_report, pipeline: pipeline, user: user) }

      subject { described_class.new(build: build, exit_code: nil, failure_reason: nil) }

      it 'does not track the build failed event', :snowplow do
        response = subject.execute

        expect(response.error?).to be true

        expect_no_snowplow_event
      end
    end
  end
end


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

module Ci
  class ChangeVariablesService < BaseContainerService
    def execute
      container.update(params)
    end
  end
end

::Ci::ChangeVariablesService.prepend_mod_with('Ci::ChangeVariablesService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ChangeVariablesService, feature_category: :secrets_management do
  let(:service) { described_class.new(container: group, current_user: user, params: params) }

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

  let(:group) { spy(:group, variables: []) }
  let(:params) { { variables_attributes: [{ key: 'new_variable', value: 'variable_value' }] } }

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

    it 'delegates to ActiveRecord update' do
      execute

      expect(group).to have_received(:update).with(params)
    end
  end
end


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

module Ci
  class RetryJobService < ::BaseService
    include Gitlab::Utils::StrongMemoize

    def execute(job, variables: [])
      if job.retryable?
        job.ensure_scheduling_type!
        new_job = retry_job(job, variables: variables)

        ServiceResponse.success(payload: { job: new_job })
      else
        ServiceResponse.error(
          message: 'Job cannot be retried',
          payload: { job: job, reason: :not_retryable }
        )
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def clone!(job, variables: [], enqueue_if_actionable: false, start_pipeline: false)
      # Cloning a job requires a strict type check to ensure
      # the attributes being used for the clone are taken straight
      # from the model and not overridden by other abstractions.
      raise TypeError unless job.instance_of?(Ci::Build) || job.instance_of?(Ci::Bridge)

      check_access!(job)

      new_job = job.clone(current_user: current_user, new_job_variables_attributes: variables)
      if enqueue_if_actionable && new_job.action?
        new_job.set_enqueue_immediately!
      end

      start_pipeline_proc = -> { start_pipeline(job, new_job) } if start_pipeline

      new_job.run_after_commit do
        start_pipeline_proc&.call

        ::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)

        ::MergeRequests::AddTodoWhenBuildFailsService
          .new(project: project)
          .close(new_job)
      end

      ::Ci::Pipelines::AddJobService.new(job.pipeline).execute!(new_job) do |processable|
        BulkInsertableAssociations.with_bulk_insert do
          processable.save!
        end
      end

      job.reset # refresh the data to get new values of `retried` and `processed`.

      new_job
    end
    # rubocop: enable CodeReuse/ActiveRecord

    private

    def check_assignable_runners!(job); end

    def retry_job(job, variables: [])
      clone!(job, variables: variables, enqueue_if_actionable: true, start_pipeline: true).tap do |new_job|
        check_assignable_runners!(new_job) if new_job.is_a?(Ci::Build)

        next if new_job.failed?

        ResetSkippedJobsService.new(project, current_user).execute(job)
      end
    end

    def check_access!(job)
      unless can?(current_user, :update_build, job)
        raise Gitlab::Access::AccessDeniedError, '403 Forbidden'
      end
    end

    def start_pipeline(job, new_job)
      Ci::PipelineCreation::StartPipelineService.new(job.pipeline).execute
      new_job.reset
    end
  end
end

Ci::RetryJobService.prepend_mod_with('Ci::RetryJobService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
  using RSpec::Parameterized::TableSyntax
  let_it_be(:reporter) { create(:user) }
  let_it_be(:developer) { create(:user) }
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:pipeline) do
    create(:ci_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
  end

  let_it_be(:stage) do
    create(:ci_stage, pipeline: pipeline, name: 'test')
  end

  let_it_be(:deploy_stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy', position: stage.position + 1) }

  let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
  let(:user) { developer }

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

  before_all do
    project.add_developer(developer)
    project.add_reporter(reporter)
  end

  shared_context 'retryable bridge' do
    let_it_be(:downstream_project) { create(:project, :repository) }

    let_it_be_with_refind(:job) do
      create(:ci_bridge, :success,
        pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage
      )
    end

    let_it_be(:job_to_clone) { job }

    before do
      job.update!(retried: false)
    end
  end

  shared_context 'retryable build' do
    let_it_be_with_reload(:job) do
      create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
    end

    let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }

    let_it_be(:job_to_clone) do
      create(
        :ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
        :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
        description: 'my-job', ci_stage: stage,
        pipeline: pipeline, auto_canceled_by: another_pipeline,
        scheduled_at: 10.seconds.since
      )
    end

    before do
      job.update!(retried: false, status: :success)
      job_to_clone.update!(retried: false, status: :success)
    end
  end

  shared_examples_for 'clones the job' do
    let(:job) { job_to_clone }

    before_all do
      job_to_clone.update!(ci_stage: stage)

      create(:ci_build_need, build: job_to_clone)
    end

    context 'when the user has ability to execute job' do
      before do
        stub_not_protect_default_branch
      end

      context 'when there is a failed job ToDo for the MR' do
        let!(:merge_request) { create(:merge_request, source_project: project, author: user, head_pipeline: pipeline) }
        let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: user, target: merge_request) }

        it 'resolves the ToDo for the failed job' do
          expect do
            service.execute(job)
          end.to change { todo.reload.state }.from('pending').to('done')
        end
      end

      context 'when the job has needs' do
        before do
          create_list(:ci_build_need, 2, build: job)
        end

        it 'bulk inserts all the needs' do
          expect(Ci::BuildNeed).to receive(:bulk_insert!).and_call_original

          new_job
        end
      end

      it 'marks the old job as retried' do
        expect(new_job).to be_latest
        expect(job).to be_retried
        expect(job).to be_processed
      end
    end

    context 'when the user does not have permission to execute the job' do
      let(:user) { reporter }

      it 'raises an error' do
        expect { service.execute(job) }
          .to raise_error Gitlab::Access::AccessDeniedError
      end
    end
  end

  shared_examples_for 'does not retry the job' do
    it 'returns :not_retryable and :unprocessable_entity' do
      expect(subject.message).to be('Job cannot be retried')
      expect(subject.payload[:reason]).to eq(:not_retryable)
      expect(subject.payload[:job]).to eq(job)
    end
  end

  shared_examples_for 'retries the job' do
    it_behaves_like 'clones the job'

    it 'enqueues the new job' do
      expect(new_job).to be_pending
    end

    context 'when there are subsequent processables that are skipped' do
      let!(:subsequent_build) do
        create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
      end

      let!(:subsequent_bridge) do
        create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
      end

      it 'resumes pipeline processing in the subsequent stage' do
        service.execute(job)

        expect(subsequent_build.reload).to be_created
        expect(subsequent_bridge.reload).to be_created
      end

      it 'updates ownership for subsequent builds' do
        expect { service.execute(job) }.to change { subsequent_build.reload.user }.to(user)
      end

      it 'updates ownership for subsequent bridges' do
        expect { service.execute(job) }.to change { subsequent_bridge.reload.user }.to(user)
      end
    end

    context 'when the pipeline has other jobs' do
      let!(:other_test_build) { create(:ci_build, pipeline: pipeline, ci_stage: stage) }
      let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: deploy_stage) }
      let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: other_test_build.name) }

      context 'when job has a nil scheduling_type' do
        before do
          job.pipeline.processables.update_all(scheduling_type: nil)
          job.reload
        end

        it 'populates scheduling_type of processables' do
          expect(new_job.scheduling_type).to eq('stage')
          expect(job.reload.scheduling_type).to eq('stage')
          expect(other_test_build.reload.scheduling_type).to eq('stage')
          expect(deploy.reload.scheduling_type).to eq('dag')
        end
      end

      context 'when job has scheduling_type' do
        it 'does not call populate_scheduling_type!' do
          expect(job.pipeline).not_to receive(:ensure_scheduling_type!)

          expect(new_job.scheduling_type).to eq('stage')
        end
      end
    end

    context 'when the pipeline is a child pipeline and the bridge uses strategy:depend' do
      let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
      let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: parent_pipeline, status: 'success') }
      let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) }

      it 'marks the source bridge as pending' do
        service.execute(job)

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

  shared_examples_for 'checks enqueue_immediately?' do
    it "returns enqueue_immediately" do
      subject
      expect(new_job.enqueue_immediately?).to eq enqueue_immediately
    end
  end

  shared_examples_for 'creates associations for a deployable job' do |factory_type|
    context 'when a job with a deployment is retried' do
      let!(:job) do
        create(factory_type, :with_deployment, :deploy_to_production, pipeline: pipeline, ci_stage: stage)
      end

      it 'creates a new deployment' do
        expect { new_job }.to change { Deployment.count }.by(1)
      end

      it 'does not create a new environment' do
        expect { new_job }.not_to change { Environment.count }
      end
    end

    context 'when a job with a dynamic environment is retried' do
      let_it_be(:other_developer) { create(:user).tap { |u| project.add_developer(u) } }

      let(:environment_name) { 'review/$CI_COMMIT_REF_SLUG-$GITLAB_USER_ID' }

      let!(:job) do
        create(factory_type, :with_deployment,
          environment: environment_name,
          options: { environment: { name: environment_name } },
          pipeline: pipeline,
          ci_stage: stage,
          user: other_developer)
      end

      it 'creates a new deployment' do
        expect { new_job }.to change { Deployment.count }.by(1)
      end

      it 'does not create a new environment' do
        expect { new_job }.not_to change { Environment.count }
      end
    end
  end

  describe '#clone!' do
    let(:start_pipeline_on_clone) { false }
    let(:new_job) { service.clone!(job, start_pipeline: start_pipeline_on_clone) }

    it 'raises an error when an unexpected class is passed' do
      expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError)
    end

    context 'when the job to be cloned is a bridge' do
      include_context 'retryable bridge'

      it_behaves_like 'clones the job'

      it 'does not create a new deployment' do
        expect { new_job }.not_to change { Deployment.count }
      end

      context 'when the pipeline is started automatically' do
        let(:start_pipeline_on_clone) { true }

        it_behaves_like 'creates associations for a deployable job', :ci_bridge
      end

      context 'when given variables' do
        let(:new_job) { service.clone!(job, variables: job_variables_attributes) }

        it 'does not give variables to the new bridge' do
          expect { new_job }.not_to raise_error
        end
      end
    end

    context 'when the job to be cloned is a build' do
      include_context 'retryable build'

      it_behaves_like 'clones the job'

      it 'does not create a new deployment' do
        expect { new_job }.not_to change { Deployment.count }
      end

      context 'when the pipeline is started automatically' do
        let(:start_pipeline_on_clone) { true }

        it_behaves_like 'creates associations for a deployable job', :ci_build
      end

      context 'when given variables' do
        let(:new_job) { service.clone!(job, variables: job_variables_attributes) }

        context 'when the build is actionable' do
          let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) }

          it 'gives variables to the new build' do
            expect(new_job.job_variables.count).to be(1)
            expect(new_job.job_variables.first.key).to eq('MANUAL_VAR')
            expect(new_job.job_variables.first.value).to eq('manual test var')
          end
        end

        context 'when the build is not actionable' do
          let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) }

          it 'does not give variables to the new build' do
            expect(new_job.job_variables.count).to be_zero
          end
        end
      end
    end

    context 'when enqueue_if_actionable is provided' do
      let!(:job) do
        create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
      end

      let(:new_job) { subject }

      subject { service.clone!(job, enqueue_if_actionable: enqueue_if_actionable) }

      where(:enqueue_if_actionable, :trait, :enqueue_immediately) do
        true  | nil                | false
        true  | :manual            | true
        true  | :expired_scheduled | true

        false | nil                | false
        false | :manual            | false
        false | :expired_scheduled | false
      end

      with_them do
        it_behaves_like 'checks enqueue_immediately?'
      end
    end
  end

  describe '#execute' do
    let(:new_job) { subject[:job] }

    subject { service.execute(job) }

    context 'when the job to be retried is a bridge' do
      context 'and it is not retryable' do
        let_it_be(:job) { create(:ci_bridge, :failed, :reached_max_descendant_pipelines_depth) }

        it_behaves_like 'does not retry the job'
      end

      include_context 'retryable bridge'

      it_behaves_like 'retries the job'

      context 'when given variables' do
        let(:new_job) { service.clone!(job, variables: job_variables_attributes) }

        it 'does not give variables to the new bridge' do
          expect { new_job }.not_to raise_error
        end
      end
    end

    context 'when the job to be retried is a build' do
      context 'and it is not retryable' do
        let_it_be(:job) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }

        it_behaves_like 'does not retry the job'
      end

      include_context 'retryable build'

      it_behaves_like 'retries the job'

      context 'automatic retryable build' do
        let!(:auto_retryable_build) do
          create(:ci_build, pipeline: pipeline, ci_stage: stage, user: user, options: { retry: 1 })
        end

        def drop_build!
          auto_retryable_build.drop_with_exit_code!('test failure', 1)
        end

        it 'creates a new build and enqueues BuildQueueWorker' do
          expect { drop_build! }.to change { Ci::Build.count }.by(1)
                                .and change { BuildQueueWorker.jobs.count }.by(1)
        end
      end

      context 'when there are subsequent jobs that are skipped' do
        let!(:subsequent_build) do
          create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
        end

        let!(:subsequent_bridge) do
          create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
        end

        it 'does not cause an N+1 when updating the job ownership' do
          control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { service.execute(job) }.count

          create_list(:ci_build, 2, :skipped, pipeline: pipeline, ci_stage: deploy_stage)

          expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
        end
      end

      context 'when given variables' do
        let(:new_job) { service.clone!(job, variables: job_variables_attributes) }

        context 'when the build is actionable' do
          let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) }

          it 'gives variables to the new build' do
            expect(new_job.job_variables.count).to be(1)
            expect(new_job.job_variables.first.key).to eq('MANUAL_VAR')
            expect(new_job.job_variables.first.value).to eq('manual test var')
          end
        end

        context 'when the build is not actionable' do
          let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) }

          it 'does not give variables to the new build' do
            expect(new_job.job_variables.count).to be_zero
          end
        end
      end
    end

    context 'when job being retried has jobs in previous stages' do
      let!(:job) do
        create(
          :ci_build,
          :failed,
          name: 'deploy_a',
          pipeline: pipeline,
          ci_stage: deploy_stage
        )
      end

      before do
        create(
          :ci_build,
          previous_stage_job_status,
          name: 'test_a',
          pipeline: pipeline,
          ci_stage: stage
        )
      end

      where(:previous_stage_job_status, :after_status) do
        :created   | 'created'
        :pending   | 'created'
        :running   | 'created'
        :manual    | 'created'
        :scheduled | 'created'
        :success   | 'pending'
        :failed    | 'skipped'
        :skipped   | 'pending'
      end

      with_them do
        it 'updates the new job status to after_status' do
          expect(subject).to be_success
          expect(new_job.status).to eq after_status
        end
      end
    end

    context 'when job being retried has DAG dependencies' do
      let!(:job) do
        create(
          :ci_build,
          :failed,
          :dependent,
          name: 'deploy_a',
          pipeline: pipeline,
          ci_stage: deploy_stage,
          needed: dependency
        )
      end

      let(:dependency) do
        create(
          :ci_build,
          dag_dependency_status,
          name: 'test_a',
          pipeline: pipeline,
          ci_stage: stage
        )
      end

      where(:dag_dependency_status, :after_status) do
        :created   | 'created'
        :pending   | 'created'
        :running   | 'created'
        :manual    | 'created'
        :scheduled | 'created'
        :success   | 'pending'
        :failed    | 'skipped'
        :skipped   | 'skipped'
      end

      with_them do
        it 'updates the new job status to after_status' do
          expect(subject).to be_success
          expect(new_job.status).to eq after_status
        end
      end
    end

    context 'when there are other manual/scheduled jobs' do
      let_it_be(:test_manual_build) do
        create(:ci_build, :manual, pipeline: pipeline, ci_stage: stage)
      end

      let_it_be(:subsequent_manual_build) do
        create(:ci_build, :manual, pipeline: pipeline, ci_stage: deploy_stage)
      end

      let_it_be(:test_scheduled_build) do
        create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: stage)
      end

      let_it_be(:subsequent_scheduled_build) do
        create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: deploy_stage)
      end

      let!(:job) do
        create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
      end

      where(:trait, :enqueue_immediately) do
        nil                | false
        :manual            | true
        :expired_scheduled | true
      end

      with_them do
        it 'retries the given job but not the other manual/scheduled jobs' do
          expect { subject }
            .to change { Ci::Build.count }.by(1)
            .and not_change { test_manual_build.reload.status }
            .and not_change { subsequent_manual_build.reload.status }
            .and not_change { test_scheduled_build.reload.status }
            .and not_change { subsequent_scheduled_build.reload.status }

          expect(new_job).to be_pending
        end

        it_behaves_like 'checks enqueue_immediately?'
      end
    end
  end
end


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

module Ci
  class DeleteUnitTestsService
    include EachBatch

    BATCH_SIZE = 100

    def execute
      purge_data!(Ci::UnitTestFailure)
      purge_data!(Ci::UnitTest)
    end

    private

    def purge_data!(klass)
      loop do
        break unless delete_batch!(klass)
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def delete_batch!(klass)
      deleted = 0

      klass.transaction do
        ids = klass.deletable.lock('FOR UPDATE SKIP LOCKED').limit(BATCH_SIZE).pluck(:id)
        deleted = klass.where(id: ids).delete_all if ids.any?
      end

      deleted > 0
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::DeleteUnitTestsService, feature_category: :continuous_integration do
  describe '#execute' do
    let!(:unit_test_1) { create(:ci_unit_test) }
    let!(:unit_test_2) { create(:ci_unit_test) }
    let!(:unit_test_3) { create(:ci_unit_test) }
    let!(:unit_test_4) { create(:ci_unit_test) }
    let!(:unit_test_1_recent_failure) { create(:ci_unit_test_failure, unit_test: unit_test_1) }
    let!(:unit_test_1_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_1, failed_at: 15.days.ago) }
    let!(:unit_test_2_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_2, failed_at: 15.days.ago) }
    let!(:unit_test_3_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_3, failed_at: 15.days.ago) }
    let!(:unit_test_4_old_failure) { create(:ci_unit_test_failure, unit_test: unit_test_4, failed_at: 15.days.ago) }

    before do
      stub_const("#{described_class.name}::BATCH_SIZE", 2)

      described_class.new.execute
    end

    it 'does not delete unit test failures not older than 14 days' do
      expect(unit_test_1_recent_failure.reload).to be_persisted
    end

    it 'deletes unit test failures older than 14 days' do
      ids = [
        unit_test_1_old_failure,
        unit_test_2_old_failure,
        unit_test_3_old_failure,
        unit_test_4_old_failure
      ].map(&:id)

      result = Ci::UnitTestFailure.where(id: ids)

      expect(result).to be_empty
    end

    it 'deletes unit tests that have no more associated unit test failures' do
      ids = [
        unit_test_2,
        unit_test_3,
        unit_test_4
      ].map(&:id)

      result = Ci::UnitTest.where(id: ids)

      expect(result).to be_empty
    end
  end
end


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

# This class is a simplified version of assign_nested_attributes_for_collection_association from ActiveRecord
# https://github.com/rails/rails/blob/v6.0.2.1/activerecord/lib/active_record/nested_attributes.rb#L466

module Ci
  class UpdateInstanceVariablesService
    UNASSIGNABLE_KEYS = %w[id _destroy].freeze

    def initialize(params)
      @params = params[:variables_attributes]
    end

    def execute
      instantiate_records
      persist_records
    end

    def errors
      @records.to_a.flat_map { |r| r.errors.full_messages }
    end

    private

    attr_reader :params

    def existing_records_by_id
      @existing_records_by_id ||= Ci::InstanceVariable
        .all
        .index_by { |var| var.id.to_s }
    end

    def instantiate_records
      @records = params.map do |attributes|
        find_or_initialize_record(attributes).tap do |record|
          record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
          record.mark_for_destruction if has_destroy_flag?(attributes)
        end
      end
    end

    def find_or_initialize_record(attributes)
      id = attributes[:id].to_s

      if id.blank?
        Ci::InstanceVariable.new
      else
        existing_records_by_id.fetch(id) { raise ActiveRecord::RecordNotFound }
      end
    end

    def persist_records
      Ci::InstanceVariable.transaction do
        success = @records.map do |record|
          if record.marked_for_destruction?
            record.destroy
          else
            record.save
          end
        end.all?

        raise ActiveRecord::Rollback unless success

        success
      end
    end

    def has_destroy_flag?(hash)
      Gitlab::Utils.to_boolean(hash['_destroy'])
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::UpdateInstanceVariablesService, feature_category: :secrets_management do
  let(:params) { { variables_attributes: variables_attributes } }

  subject { described_class.new(params) }

  describe '#execute' do
    context 'without variables' do
      let(:variables_attributes) { [] }

      it { expect(subject.execute).to be_truthy }
    end

    context 'with insert only variables' do
      let(:variables_attributes) do
        [
          { key: 'var_a', secret_value: 'dummy_value_for_a', protected: true },
          { key: 'var_b', secret_value: 'dummy_value_for_b', protected: false }
        ]
      end

      it { expect(subject.execute).to be_truthy }

      it 'persists all the records' do
        expect { subject.execute }
          .to change { Ci::InstanceVariable.count }
          .by variables_attributes.size
      end

      it 'persists attributes' do
        subject.execute

        expect(Ci::InstanceVariable.all).to contain_exactly(
          have_attributes(key: 'var_a', secret_value: 'dummy_value_for_a', protected: true),
          have_attributes(key: 'var_b', secret_value: 'dummy_value_for_b', protected: false)
        )
      end
    end

    context 'with update only variables' do
      let!(:var_a) { create(:ci_instance_variable) }
      let!(:var_b) { create(:ci_instance_variable, protected: false) }

      let(:variables_attributes) do
        [
          {
            id: var_a.id,
            key: var_a.key,
            secret_value: 'new_dummy_value_for_a',
            protected: var_a.protected?.to_s
          },
          {
            id: var_b.id,
            key: 'var_b_key',
            secret_value: 'new_dummy_value_for_b',
            protected: 'true'
          }
        ]
      end

      it { expect(subject.execute).to be_truthy }

      it 'does not change the count' do
        expect { subject.execute }
          .not_to change { Ci::InstanceVariable.count }
      end

      it 'updates the records in place', :aggregate_failures do
        subject.execute

        expect(var_a.reload).to have_attributes(secret_value: 'new_dummy_value_for_a')

        expect(var_b.reload).to have_attributes(
          key: 'var_b_key', secret_value: 'new_dummy_value_for_b', protected: true)
      end
    end

    context 'with insert and update variables' do
      let!(:var_a) { create(:ci_instance_variable) }

      let(:variables_attributes) do
        [
          {
            id: var_a.id,
            key: var_a.key,
            secret_value: 'new_dummy_value_for_a',
            protected: var_a.protected?.to_s
          },
          {
            key: 'var_b',
            secret_value: 'dummy_value_for_b',
            protected: true
          }
        ]
      end

      it { expect(subject.execute).to be_truthy }

      it 'inserts only one record' do
        expect { subject.execute }
          .to change { Ci::InstanceVariable.count }.by 1
      end

      it 'persists all the records', :aggregate_failures do
        subject.execute
        var_b = Ci::InstanceVariable.find_by(key: 'var_b')

        expect(var_a.reload.secret_value).to eq('new_dummy_value_for_a')
        expect(var_b.secret_value).to eq('dummy_value_for_b')
      end
    end

    context 'with insert, update, and destroy variables' do
      let!(:var_a) { create(:ci_instance_variable) }
      let!(:var_b) { create(:ci_instance_variable) }

      let(:variables_attributes) do
        [
          {
            id: var_a.id,
            key: var_a.key,
            secret_value: 'new_dummy_value_for_a',
            protected: var_a.protected?.to_s
          },
          {
            id: var_b.id,
            key: var_b.key,
            secret_value: 'dummy_value_for_b',
            protected: var_b.protected?.to_s,
            '_destroy' => 'true'
          },
          {
            key: 'var_c',
            secret_value: 'dummy_value_for_c',
            protected: true
          }
        ]
      end

      it { expect(subject.execute).to be_truthy }

      it 'persists all the records', :aggregate_failures do
        subject.execute
        var_c = Ci::InstanceVariable.find_by(key: 'var_c')

        expect(var_a.reload.secret_value).to eq('new_dummy_value_for_a')
        expect { var_b.reload }.to raise_error(ActiveRecord::RecordNotFound)
        expect(var_c.secret_value).to eq('dummy_value_for_c')
      end
    end

    context 'with invalid variables' do
      let!(:var_a) { create(:ci_instance_variable, secret_value: 'dummy_value_for_a') }

      let(:variables_attributes) do
        [
          {
            key: '...?',
            secret_value: 'nice_value'
          },
          {
            id: var_a.id,
            key: var_a.key,
            secret_value: 'new_dummy_value_for_a',
            protected: var_a.protected?.to_s
          },
          {
            key: var_a.key,
            secret_value: 'other_value'
          }
        ]
      end

      it { expect(subject.execute).to be_falsey }

      it 'does not insert any records' do
        expect { subject.execute }
          .not_to change { Ci::InstanceVariable.count }
      end

      it 'does not update existing records' do
        subject.execute

        expect(var_a.reload.secret_value).to eq('dummy_value_for_a')
      end

      it 'returns errors' do
        subject.execute

        expect(subject.errors).to match_array(
          [
            "Key (#{var_a.key}) has already been taken",
            "Key can contain only letters, digits and '_'."
          ])
      end
    end

    context 'when deleting non existing variables' do
      let(:variables_attributes) do
        [
          {
            id: 'some-id',
            key: 'some_key',
            secret_value: 'other_value',
            '_destroy' => 'true'
          }
        ]
      end

      it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
    end

    context 'when updating non existing variables' do
      let(:variables_attributes) do
        [
          {
            id: 'some-id',
            key: 'some_key',
            secret_value: 'other_value'
          }
        ]
      end

      it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
    end
  end
end


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

module Ci
  # TODO: a couple of points with this approach:
  # + reuses existing architecture and reactive caching
  # - it's not a report comparison and some comparing features must be turned off.
  # see CompareReportsBaseService for more notes.
  # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
  class GenerateCoverageReportsService < CompareReportsBaseService
    def execute(base_pipeline, head_pipeline)
      merge_request = MergeRequest.find_by_id(params[:id])
      {
        status: :parsed,
        key: key(base_pipeline, head_pipeline),
        data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_coverage).present.for_files(merge_request.new_paths)
      }
    rescue StandardError => e
      Gitlab::ErrorTracking.track_exception(
        e,
        project_id: project.id,
        base_pipeline_id: base_pipeline&.id,
        head_pipeline_id: head_pipeline&.id
      )

      {
        status: :error,
        key: key(base_pipeline, head_pipeline),
        status_reason: _('An error occurred while fetching coverage reports.')
      }
    end

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

    private

    def key(base_pipeline, head_pipeline)
      [
        base_pipeline&.id, last_update_timestamp(base_pipeline),
        head_pipeline&.id, last_update_timestamp(head_pipeline)
      ]
    end

    def last_update_timestamp(pipeline_hierarchy)
      pipeline_hierarchy&.self_and_project_descendants&.maximum(:updated_at)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

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

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

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

    context 'when head pipeline has coverage reports' do
      let!(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) }
      let!(:service) { described_class.new(project, nil, id: merge_request.id) }
      let!(:head_pipeline) { merge_request.head_pipeline }
      let!(:base_pipeline) { nil }

      it 'returns status and data', :aggregate_failures do
        expect_any_instance_of(Ci::PipelineArtifact) do |instance|
          expect(instance).to receive(:present)
          expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original
        end

        expect(subject[:status]).to eq(:parsed)
        expect(subject[:data]).to eq(files: {})
      end
    end

    context 'when head pipeline does not have a coverage report artifact' do
      let!(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) }
      let!(:service) { described_class.new(project, nil, id: merge_request.id) }
      let!(:head_pipeline) { merge_request.head_pipeline }
      let!(:base_pipeline) { nil }

      before do
        head_pipeline.pipeline_artifacts.destroy_all # rubocop: disable Cop/DestroyAll
      end

      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:status_reason]).to include('An error occurred while fetching coverage reports.')
      end
    end

    context 'when head pipeline has coverage reports and no merge request associated' do
      let!(:head_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
      let!(:base_pipeline) { nil }

      it 'returns status and error message' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:status_reason]).to include('An error occurred while fetching coverage reports.')
      end
    end
  end

  describe '#latest?' do
    subject { service.latest?(base_pipeline, head_pipeline, data) }

    let!(:base_pipeline) { nil }
    let!(:head_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
    let!(:child_pipeline) { create(:ci_pipeline, child_of: head_pipeline) }
    let!(:key) { service.send(:key, base_pipeline, head_pipeline) }

    let(:data) { { key: key } }

    context 'when cache key is latest' do
      it { is_expected.to be_truthy }
    end

    context 'when head pipeline has been updated' do
      before do
        head_pipeline.update_column(:updated_at, 1.minute.from_now)
      end

      it { is_expected.to be_falsy }
    end

    context 'when cache key is empty' do
      let(:data) { { key: nil } }

      it { is_expected.to be_falsy }
    end

    context 'when the pipeline has a child that is updated' do
      before do
        child_pipeline.update_column(:updated_at, 1.minute.from_now)
      end

      it { is_expected.to be_falsy }
    end
  end
end


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

module Ci
  class ProcessPipelineService
    attr_reader :pipeline

    def initialize(pipeline)
      @pipeline = pipeline
    end

    def execute
      increment_processing_counter

      Ci::PipelineProcessing::AtomicProcessingService
        .new(pipeline)
        .execute
    end

    def metrics
      @metrics ||= ::Gitlab::Ci::Pipeline::Metrics
    end

    private

    def increment_processing_counter
      metrics.pipeline_processing_events_counter.increment
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

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

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

  let(:pipeline_processing_events_counter) { double(increment: true) }

  let(:metrics) do
    double(pipeline_processing_events_counter: pipeline_processing_events_counter)
  end

  subject { described_class.new(pipeline) }

  before do
    stub_ci_pipeline_to_return_yaml_file
    stub_not_protect_default_branch

    allow(subject).to receive(:metrics).and_return(metrics)
  end

  describe 'processing events counter' do
    it 'increments processing events counter' do
      expect(pipeline_processing_events_counter).to receive(:increment)

      subject.execute
    end
  end
end


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

module Ci
  class CreateWebIdeTerminalService < ::BaseService
    include ::Gitlab::Utils::StrongMemoize

    TerminalCreationError = Class.new(StandardError)

    TERMINAL_NAME = 'terminal'

    attr_reader :terminal

    def execute
      check_access!
      validate_params!
      load_terminal_config!

      pipeline = create_pipeline!
      success(pipeline: pipeline)
    rescue TerminalCreationError => e
      error(e.message)
    rescue ActiveRecord::RecordInvalid => e
      error("Failed to persist the pipeline: #{e.message}")
    end

    private

    def create_pipeline!
      build_pipeline.tap do |pipeline|
        pipeline.stages << terminal_stage_seed(pipeline).to_resource

        # Project iid must be called outside a transaction, so we ensure it is set here
        # otherwise it may be set within the save! which it will lock the InternalId row for the whole transaction
        pipeline.ensure_project_iid!

        pipeline.save!

        Ci::ProcessPipelineService
          .new(pipeline)
          .execute

        pipeline_created_counter.increment(source: :webide)
      end
    end

    def build_pipeline
      Ci::Pipeline.new(
        project: project,
        user: current_user,
        source: :webide,
        config_source: :webide_source,
        ref: ref,
        sha: sha,
        tag: false,
        before_sha: Gitlab::Git::BLANK_SHA
      )
    end

    def terminal_stage_seed(pipeline)
      attributes = {
        name: TERMINAL_NAME,
        index: 0,
        builds: [terminal_build_seed]
      }

      seed_context = Gitlab::Ci::Pipeline::Seed::Context.new(pipeline)
      Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, attributes, [])
    end

    def terminal_build_seed
      terminal.merge(
        name: TERMINAL_NAME,
        stage: TERMINAL_NAME,
        user: current_user,
        scheduling_type: :stage)
    end

    def load_terminal_config!
      result = ::Ide::TerminalConfigService.new(project, current_user, sha: sha).execute
      raise TerminalCreationError, result[:message] if result[:status] != :success

      @terminal = result[:terminal]
      raise TerminalCreationError, 'Terminal is not configured' unless terminal
    end

    def validate_params!
      unless sha
        raise TerminalCreationError, 'Ref does not exist'
      end

      unless branch_exists?
        raise TerminalCreationError, 'Ref needs to be a branch'
      end
    end

    def check_access!
      unless can?(current_user, :create_web_ide_terminal, project)
        raise TerminalCreationError, 'Insufficient permissions to create a terminal'
      end

      if terminal_active?
        raise TerminalCreationError, 'There is already a terminal running'
      end
    end

    def pipeline_created_counter
      @pipeline_created_counter ||= Gitlab::Metrics
        .counter(:pipelines_created_total, "Counter of pipelines created")
    end

    def terminal_active?
      project.active_webide_pipelines(user: current_user).exists?
    end

    def ref
      strong_memoize(:ref) do
        Gitlab::Git.ref_name(params[:ref])
      end
    end

    def branch_exists?
      project.repository.branch_exists?(ref)
    end

    def sha
      project.commit(params[:ref]).try(:id)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

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

  let(:ref) { 'master' }

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

    context 'for maintainer' do
      shared_examples 'be successful' do
        it 'returns a success with pipeline object' do
          is_expected.to include(status: :success)

          expect(subject[:pipeline]).to be_a(Ci::Pipeline)
          expect(subject[:pipeline]).to be_persisted
          expect(subject[:pipeline].stages.count).to eq(1)
          expect(subject[:pipeline].builds.count).to eq(1)
        end

        it 'calls ensure_project_iid explicitly' do
          expect_next_instance_of(Ci::Pipeline) do |instance|
            expect(instance).to receive(:ensure_project_iid!).twice
          end
          subject
        end
      end

      before do
        project.add_maintainer(user)
      end

      context 'when web-ide has valid configuration' do
        before do
          stub_webide_config_file(config_content)
        end

        context 'for empty configuration' do
          let(:config_content) do
            'terminal: {}'
          end

          it_behaves_like 'be successful'
        end

        context 'for configuration with container image' do
          let(:config_content) do
            'terminal: { image: ruby }'
          end

          it_behaves_like 'be successful'
        end

        context 'for configuration with ports' do
          let(:config_content) do
            <<-EOS
              terminal:
                image:
                  name: image:1.0
                  ports:
                    - 80
                script: rspec
                services:
                  - name: test
                    alias: test
                    ports:
                      - 8080
            EOS
          end

          it_behaves_like 'be successful'
        end

        context 'for configuration with variables' do
          let(:config_content) do
            <<-EOS
              terminal:
                script: rspec
                variables:
                  KEY1: VAL1
            EOS
          end

          it_behaves_like 'be successful'

          it 'saves the variables' do
            expect(subject[:pipeline].builds[0].variables).to include(
              key: 'KEY1', value: 'VAL1', public: true, masked: false
            )
          end
        end
      end
    end

    context 'error handling' do
      shared_examples 'having an error' do |message|
        it 'returns an error' do
          is_expected.to eq(
            status: :error,
            message: message
          )
        end
      end

      shared_examples 'having insufficient permissions' do
        it_behaves_like 'having an error', 'Insufficient permissions to create a terminal'
      end

      context 'when user is developer' do
        before do
          project.add_developer(user)
        end

        it_behaves_like 'having insufficient permissions'
      end

      context 'when user is maintainer' do
        before do
          project.add_maintainer(user)
        end

        context 'when terminal is already running' do
          let!(:webide_pipeline) { create(:ci_pipeline, :webide, :running, project: project, user: user) }

          it_behaves_like 'having an error', 'There is already a terminal running'
        end

        context 'when ref is non-existing' do
          let(:ref) { 'non-existing-ref' }

          it_behaves_like 'having an error', 'Ref does not exist'
        end

        context 'when ref is a tag' do
          let(:ref) { 'v1.0.0' }

          it_behaves_like 'having an error', 'Ref needs to be a branch'
        end

        context 'when terminal config is missing' do
          let(:ref) { 'v1.0.0' }

          it_behaves_like 'having an error', 'Ref needs to be a branch'
        end

        context 'when webide config is present' do
          before do
            stub_webide_config_file(config_content)
          end

          context 'config has invalid content' do
            let(:config_content) { 'invalid' }

            it_behaves_like 'having an error', 'Invalid configuration format'
          end

          context 'config is valid, but does not have terminal' do
            let(:config_content) { '{}' }

            it_behaves_like 'having an error', 'Terminal is not configured'
          end
        end
      end
    end
  end
end