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

module Ci
  module Runners
    class UnregisterRunnerService
      attr_reader :runner, :author

      # @param [Ci::Runner] runner the runner to unregister/destroy
      # @param [User, authentication token String] author the user or the authentication token that authorizes the removal
      def initialize(runner, author)
        @runner = runner
        @author = author
      end

      def execute
        runner.destroy!

        ServiceResponse.success
      end
    end
  end
end

Ci::Runners::UnregisterRunnerService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::UnregisterRunnerService, '#execute', feature_category: :runner_fleet do
  subject(:execute) { described_class.new(runner, 'some_token').execute }

  let(:runner) { create(:ci_runner) }

  it 'destroys runner' do
    expect(runner).to receive(:destroy).once.and_call_original

    expect do
      expect(execute).to be_success
    end.to change { Ci::Runner.count }.by(-1)
    expect(runner[:errors]).to be_nil
  end
end


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

module Ci
  module Runners
    class StaleManagersCleanupService
      MAX_DELETIONS = 1000
      SUB_BATCH_LIMIT = 100

      def execute
        ServiceResponse.success(payload: delete_stale_runner_managers)
      end

      private

      def delete_stale_runner_managers
        batch_counts = []
        total_deleted_count = 0
        loop do
          sub_batch_limit = [SUB_BATCH_LIMIT, MAX_DELETIONS].min

          # delete_all discards part of the `stale` scope query, so we explicitly wrap it with a SELECT as a workaround
          deleted_count = Ci::RunnerManager.id_in(Ci::RunnerManager.stale.limit(sub_batch_limit)).delete_all
          batch_counts << deleted_count
          total_deleted_count += deleted_count

          break if deleted_count == 0 || total_deleted_count >= MAX_DELETIONS
        end

        {
          total_deleted: total_deleted_count,
          batch_counts: batch_counts
        }
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Runners::StaleManagersCleanupService, feature_category: :runner_fleet do
  let(:service) { described_class.new }
  let!(:runner_manager3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }

  subject(:response) { service.execute }

  context 'with no stale runner managers' do
    it 'does not clean any runner managers and returns :success status' do
      expect do
        expect(response).to be_success
        expect(response.payload).to match({ total_deleted: 0, batch_counts: [0] })
      end.not_to change { Ci::RunnerManager.count }.from(1)
    end
  end

  context 'with some stale runner managers' do
    before do
      create(:ci_runner_machine, :stale)
      create(:ci_runner_machine, :stale, contacted_at: nil)
    end

    it 'only leaves non-stale runners' do
      expect(response).to be_success
      expect(response.payload).to match({ total_deleted: 2, batch_counts: [2, 0] })
      expect(Ci::RunnerManager.all).to contain_exactly(runner_manager3)
    end

    context 'with more stale runners than SUB_BATCH_LIMIT' do
      before do
        stub_const("#{described_class}::SUB_BATCH_LIMIT", 1)
      end

      it 'only leaves non-stale runners' do
        expect(response).to be_success
        expect(response.payload).to match({ total_deleted: 2, batch_counts: [1, 1, 0] })
        expect(Ci::RunnerManager.all).to contain_exactly(runner_manager3)
      end
    end

    context 'with more stale runners than MAX_DELETIONS' do
      before do
        stub_const("#{described_class}::MAX_DELETIONS", 1)
      end

      it 'only leaves non-stale runners' do
        expect do
          expect(response).to be_success
          expect(response.payload).to match({
            total_deleted: Ci::Runners::StaleManagersCleanupService::MAX_DELETIONS,
            batch_counts: [1]
          })
        end.to change { Ci::RunnerManager.count }.by(-Ci::Runners::StaleManagersCleanupService::MAX_DELETIONS)
      end
    end
  end
end


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

module Ci
  module Runners
    class RegisterRunnerService
      include Gitlab::Utils::StrongMemoize

      def initialize(registration_token, attributes)
        @registration_token = registration_token
        @attributes = attributes
      end

      def execute
        return ServiceResponse.error(message: 'invalid token supplied', http_status: :forbidden) unless attrs_from_token

        unless registration_token_allowed?(attrs_from_token)
          return ServiceResponse.error(
            message: 'runner registration disallowed',
            reason: :runner_registration_disallowed)
        end

        runner = ::Ci::Runner.new(attributes.merge(attrs_from_token))

        Ci::BulkInsertableTags.with_bulk_insert_tags do
          Ci::Runner.transaction do
            if runner.save
              Gitlab::Ci::Tags::BulkInsert.bulk_insert_tags!([runner])
            else
              raise ActiveRecord::Rollback
            end
          end
        end

        ServiceResponse.success(payload: { runner: runner })
      end

      private

      attr_reader :registration_token, :attributes

      def attrs_from_token
        if runner_registration_token_valid?(registration_token)
          # Create shared runner. Requires admin access
          { runner_type: :instance_type }
        elsif runner_registrar_valid?('project') && project = ::Project.find_by_runners_token(registration_token)
          # Create a project runner
          { runner_type: :project_type, projects: [project] }
        elsif runner_registrar_valid?('group') && group = ::Group.find_by_runners_token(registration_token)
          # Create a group runner
          { runner_type: :group_type, groups: [group] }
        end
      end
      strong_memoize_attr :attrs_from_token

      def registration_token_allowed?(attrs)
        case attrs[:runner_type]
        when :group_type
          token_scope.allow_runner_registration_token?
        when :project_type
          token_scope.namespace.allow_runner_registration_token?
        else
          Gitlab::CurrentSettings.allow_runner_registration_token
        end
      end

      def runner_registration_token_valid?(registration_token)
        ActiveSupport::SecurityUtils.secure_compare(registration_token, Gitlab::CurrentSettings.runners_registration_token)
      end

      def runner_registrar_valid?(type)
        Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
      end

      def token_scope
        case attrs_from_token[:runner_type]
        when :project_type
          attrs_from_token[:projects]&.first
        when :group_type
          attrs_from_token[:groups]&.first
          # No scope for instance type
        end
      end
    end
  end
end

Ci::Runners::RegisterRunnerService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_category: :runner_fleet do
  let(:registration_token) { 'abcdefg123456' }
  let(:token) {}
  let(:args) { {} }
  let(:runner) { execute.payload[:runner] }
  let(:allow_runner_registration_token) { true }

  before do
    stub_application_setting(runners_registration_token: registration_token)
    stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES)
    stub_application_setting(allow_runner_registration_token: allow_runner_registration_token)
  end

  subject(:execute) { described_class.new(token, args).execute }

  shared_examples 'runner registration is disallowed' do
    it 'returns error response with runner_registration_disallowed reason' do
      expect(execute).to be_error
      expect(execute.message).to eq 'runner registration disallowed'
      expect(execute.reason).to eq :runner_registration_disallowed
    end
  end

  context 'when no token is provided' do
    let(:token) { '' }

    it 'returns error response' do
      expect(execute).to be_error
      expect(execute.message).to eq 'invalid token supplied'
      expect(execute.http_status).to eq :forbidden
    end
  end

  context 'when invalid token is provided' do
    let(:token) { 'invalid' }

    it 'returns error response' do
      expect(execute).to be_error
      expect(execute.message).to eq 'invalid token supplied'
      expect(execute.http_status).to eq :forbidden
    end
  end

  context 'when valid token is provided' do
    context 'when instance registration token is used' do
      let(:token) { registration_token }

      it 'creates runner with default values' do
        expect(execute).to be_success

        expect(runner).to be_an_instance_of(::Ci::Runner)
        expect(runner.persisted?).to be_truthy
        expect(runner.run_untagged).to be true
        expect(runner.active).to be true
        expect(runner.token).not_to eq(registration_token)
        expect(runner.token).not_to start_with(::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX)
        expect(runner).to be_instance_type
      end

      context 'when registering instance runners is disallowed' do
        let(:allow_runner_registration_token) { false }

        it_behaves_like 'runner registration is disallowed'
      end

      context 'with non-default arguments' do
        let(:args) do
          {
            description: 'some description',
            active: false,
            locked: true,
            run_untagged: false,
            tag_list: %w[tag1 tag2],
            access_level: 'ref_protected',
            maximum_timeout: 600,
            name: 'some name',
            version: 'some version',
            revision: 'some revision',
            platform: 'some platform',
            architecture: 'some architecture',
            ip_address: '10.0.0.1',
            config: {
              gpus: 'some gpu config'
            }
          }
        end

        it 'creates runner with specified values', :aggregate_failures do
          expect(execute).to be_success

          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.active).to eq args[:active]
          expect(runner.locked).to eq args[:locked]
          expect(runner.run_untagged).to eq args[:run_untagged]
          expect(runner.tags).to contain_exactly(
            an_object_having_attributes(name: 'tag1'),
            an_object_having_attributes(name: 'tag2')
          )
          expect(runner.access_level).to eq args[:access_level]
          expect(runner.maximum_timeout).to eq args[:maximum_timeout]
          expect(runner.name).to eq args[:name]
          expect(runner.version).to eq args[:version]
          expect(runner.revision).to eq args[:revision]
          expect(runner.platform).to eq args[:platform]
          expect(runner.architecture).to eq args[:architecture]
          expect(runner.ip_address).to eq args[:ip_address]

          expect(Ci::Runner.tagged_with('tag1')).to include(runner)
          expect(Ci::Runner.tagged_with('tag2')).to include(runner)
        end
      end

      context 'with runner token expiration interval', :freeze_time do
        before do
          stub_application_setting(runner_token_expiration_interval: 5.days)
        end

        it 'creates runner with token expiration' do
          expect(execute).to be_success

          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.token_expires_at).to eq(5.days.from_now)
        end
      end
    end

    context 'when project registration token is used' do
      let_it_be(:project) { create(:project, :with_namespace_settings) }

      let(:token) { project.runners_token }
      let(:allow_group_runner_registration_token) { true }

      before do
        project.namespace.update!(allow_runner_registration_token: allow_group_runner_registration_token)
      end

      it 'creates project runner' do
        expect(execute).to be_success

        expect(runner).to be_an_instance_of(::Ci::Runner)
        expect(project.runners.size).to eq(1)
        expect(runner).to eq(project.runners.first)
        expect(runner.token).not_to eq(registration_token)
        expect(runner.token).not_to eq(project.runners_token)
        expect(runner).to be_project_type
      end

      context 'with runner registration disabled at instance level' do
        let(:allow_runner_registration_token) { false }

        it_behaves_like 'runner registration is disallowed'
      end

      context 'with runner registration disabled at group level' do
        let(:allow_group_runner_registration_token) { false }

        it_behaves_like 'runner registration is disallowed'
      end

      context 'when it exceeds the application limits' do
        before do
          create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
          create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
        end

        it 'does not create runner' do
          expect(execute).to be_success

          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.persisted?).to be_falsey
          expect(runner.errors.messages).to eq(
            'runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded']
          )
          expect(project.runners.reload.size).to eq(1)
        end
      end

      context 'when abandoned runners cause application limits to not be exceeded' do
        before do
          create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
          create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
        end

        it 'creates runner' do
          expect(execute).to be_success

          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.errors).to be_empty
          expect(project.runners.reload.size).to eq(2)
          expect(project.runners.recent.size).to eq(1)
        end
      end

      context 'when valid runner registrars do not include project' do
        before do
          stub_application_setting(valid_runner_registrars: ['group'])
        end

        it 'returns 403 error' do
          expect(execute).to be_error
          expect(execute.http_status).to eq :forbidden
        end
      end
    end

    context 'when group registration token is used' do
      let_it_be_with_refind(:group) { create(:group) }

      let(:token) { group.runners_token }
      let(:allow_group_runner_registration_token) { true }

      before do
        group.update!(allow_runner_registration_token: allow_group_runner_registration_token)
      end

      it 'creates a group runner' do
        expect(execute).to be_success

        expect(runner).to be_an_instance_of(::Ci::Runner)
        expect(runner.errors).to be_empty
        expect(group.runners.reload.size).to eq(1)
        expect(runner.token).not_to eq(registration_token)
        expect(runner.token).not_to eq(group.runners_token)
        expect(runner).to be_group_type
      end

      context 'with runner registration disabled at instance level' do
        let(:allow_runner_registration_token) { false }

        it_behaves_like 'runner registration is disallowed'
      end

      context 'with runner registration disabled at group level' do
        let(:allow_group_runner_registration_token) { false }

        it_behaves_like 'runner registration is disallowed'
      end

      context 'when it exceeds the application limits' do
        before do
          create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
          create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
        end

        it 'does not create runner' do
          expect(execute).to be_success

          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.persisted?).to be_falsey
          expect(runner.errors.messages).to eq(
            'runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded']
          )
          expect(group.runners.reload.size).to eq(1)
        end
      end

      context 'when abandoned runners cause application limits to not be exceeded' do
        before do
          create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
          create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
          create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
        end

        it 'creates runner' do
          expect(execute).to be_success

          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.errors).to be_empty
          expect(group.runners.reload.size).to eq(3)
          expect(group.runners.recent.size).to eq(1)
        end
      end

      context 'when valid runner registrars do not include group' do
        before do
          stub_application_setting(valid_runner_registrars: ['project'])
        end

        it 'returns error response' do
          is_expected.to be_error
        end
      end
    end

    context 'when tags are provided' do
      let(:token) { registration_token }

      let(:args) do
        { tag_list: %w[tag1 tag2] }
      end

      it 'creates runner with tags' do
        expect(runner).to be_persisted

        expect(runner.tags).to contain_exactly(
          an_object_having_attributes(name: 'tag1'),
          an_object_having_attributes(name: 'tag2')
        )
      end

      it 'creates tags in bulk' do
        expect(Gitlab::Ci::Tags::BulkInsert).to receive(:bulk_insert_tags!).and_call_original

        expect(runner).to be_persisted
      end

      context 'and tag list exceeds limit' do
        let(:args) do
          { tag_list: (1..Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } }
        end

        it 'does not create any tags' do
          expect(Gitlab::Ci::Tags::BulkInsert).not_to receive(:bulk_insert_tags!)

          expect(runner).not_to be_persisted
          expect(runner.tags).to be_empty
        end
      end
    end
  end
end


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

module Ci
  module Runners
    class ProcessRunnerVersionUpdateService
      def initialize(version)
        @version = version
      end

      def execute
        return ServiceResponse.error(message: 'version update disabled') unless enabled?
        return ServiceResponse.error(message: 'version not present') unless @version

        _, status = upgrade_check_service.check_runner_upgrade_suggestion(@version)
        return ServiceResponse.error(message: 'upgrade version check failed') if status == :error

        Ci::RunnerVersion.upsert({ version: @version, status: status })
        ServiceResponse.success(payload: { upgrade_status: status.to_s })
      end

      private

      def upgrade_check_service
        @runner_upgrade_check ||= Gitlab::Ci::RunnerUpgradeCheck.new(::Gitlab::VERSION)
      end

      def enabled?
        Gitlab::Ci::RunnerReleases.instance.enabled?
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService, feature_category: :runner_fleet do
  subject(:service) { described_class.new(version) }

  let(:version) { '1.0.0' }
  let(:available_runner_releases) { %w[1.0.0 1.0.1] }

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

    context 'with upgrade check returning error' do
      let(:service_double) { instance_double(Gitlab::Ci::RunnerUpgradeCheck) }

      before do
        allow(service_double).to receive(:check_runner_upgrade_suggestion).with(version)
          .and_return([version, :error])
        allow(service).to receive(:upgrade_check_service).and_return(service_double)
      end

      it 'does not update ci_runner_versions records', :aggregate_failures do
        expect do
          expect(execute).to be_error
          expect(execute.message).to eq 'upgrade version check failed'
        end.not_to change(Ci::RunnerVersion, :count).from(0)
        expect(service_double).to have_received(:check_runner_upgrade_suggestion).with(version).once
      end
    end

    context 'when fetching runner releases is disabled' do
      before do
        stub_application_setting(update_runner_versions_enabled: false)
      end

      it 'does not update ci_runner_versions records', :aggregate_failures do
        expect do
          expect(execute).to be_error
          expect(execute.message).to eq 'version update disabled'
        end.not_to change(Ci::RunnerVersion, :count).from(0)
      end
    end

    context 'with successful result from upgrade check' do
      before do
        url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url

        WebMock.stub_request(:get, url).to_return(
          body: available_runner_releases.map { |v| { name: v } }.to_json,
          status: 200,
          headers: { 'Content-Type' => 'application/json' }
        )
      end

      context 'with no existing ci_runner_version record' do
        it 'creates ci_runner_versions record', :aggregate_failures do
          expect do
            expect(execute).to be_success
            expect(execute.http_status).to eq :ok
            expect(execute.payload).to eq({ upgrade_status: 'recommended' })
          end.to change(Ci::RunnerVersion, :all).to contain_exactly(
            an_object_having_attributes(version: version, status: 'recommended')
          )
        end
      end

      context 'with existing ci_runner_version record' do
        let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :unavailable) }

        it 'updates ci_runner_versions record', :aggregate_failures do
          expect do
            expect(execute).to be_success
            expect(execute.http_status).to eq :ok
            expect(execute.payload).to eq({ upgrade_status: 'recommended' })
          end.to change { runner_version.reload.status }.from('unavailable').to('recommended')
        end
      end

      context 'with up-to-date ci_runner_version record' do
        let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :recommended) }

        it 'does not update ci_runner_versions record', :aggregate_failures do
          expect do
            expect(execute).to be_success
            expect(execute.http_status).to eq :ok
            expect(execute.payload).to eq({ upgrade_status: 'recommended' })
          end.not_to change { runner_version.reload.status }
        end
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    class DeleteProjectArtifactsService < BaseProjectService
      def execute
        ExpireProjectBuildArtifactsWorker.perform_async(project.id)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

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

  subject { described_class.new(project: project) }

  describe '#execute' do
    it 'enqueues a Ci::ExpireProjectBuildArtifactsWorker' do
      expect(Ci::JobArtifacts::ExpireProjectBuildArtifactsWorker).to receive(:perform_async).with(project.id).and_call_original

      subject.execute
    end
  end
end


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

module Ci
  module JobArtifacts
    class TrackArtifactReportService
      include Gitlab::Utils::UsageData

      REPORT_TRACKED = %i[test coverage].freeze

      def execute(pipeline)
        REPORT_TRACKED.each do |report|
          if pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(report))
            track_usage_event(event_name(report), pipeline.user_id)
          end
        end
      end

      def event_name(report)
        "i_testing_#{report}_report_uploaded"
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::TrackArtifactReportService, feature_category: :build_artifacts do
  describe '#execute', :clean_gitlab_redis_shared_state do
    let_it_be(:group) { create(:group, :private) }
    let_it_be(:project) { create(:project, group: group) }
    let_it_be(:user1) { create(:user) }
    let_it_be(:user2) { create(:user) }

    let(:test_event_name_1) { 'i_testing_test_report_uploaded' }
    let(:test_event_name_2) { 'i_testing_coverage_report_uploaded' }

    let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
    let(:start_time) { 1.week.ago }
    let(:end_time) { 1.week.from_now }

    subject(:track_artifact_report) { described_class.new.execute(pipeline) }

    context 'when pipeline has test reports' do
      let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) }

      before do
        2.times do
          pipeline.builds << build(:ci_build, :test_reports, pipeline: pipeline, project: pipeline.project)
        end
      end

      it 'tracks the test event using HLLRedisCounter' do
        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_1, values: user1.id)
          .and_call_original

        expect { track_artifact_report }.to change {
          counter.unique_events(event_names: test_event_name_1, start_date: start_time, end_date: end_time)
        }.by 1
      end
    end

    context 'when pipeline does not have test reports' do
      let_it_be(:pipeline) { create(:ci_empty_pipeline) }

      it 'does not track the test event' do
        track_artifact_report

        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .not_to receive(:track_event)
          .with(anything, test_event_name_1)
      end

      it 'does not track the coverage test event' do
        track_artifact_report

        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .not_to receive(:track_event)
          .with(anything, test_event_name_2)
      end
    end

    context 'when a single user started multiple pipelines with test reports' do
      let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
      let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }

      it 'tracks all pipelines using HLLRedisCounter by one user_id for the test event' do
        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_1, values: user1.id)
          .and_call_original

        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_1, values: user1.id)
          .and_call_original

        expect do
          described_class.new.execute(pipeline1)
          described_class.new.execute(pipeline2)
        end.to change {
          counter.unique_events(event_names: test_event_name_1, start_date: start_time, end_date: end_time)
        }.by 1
      end
    end

    context 'when multiple users started multiple pipelines with test reports' do
      let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
      let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user2) }

      it 'tracks all pipelines using HLLRedisCounter by multiple users for test reports' do
        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_1, values: user1.id)
          .and_call_original

        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_1, values: user2.id)
          .and_call_original

        expect do
          described_class.new.execute(pipeline1)
          described_class.new.execute(pipeline2)
        end.to change {
          counter.unique_events(event_names: test_event_name_1, start_date: start_time, end_date: end_time)
        }.by 2
      end
    end

    context 'when pipeline has coverage test reports' do
      let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) }

      before do
        2.times do
          pipeline.builds << build(:ci_build, :coverage_reports, pipeline: pipeline, project: pipeline.project)
        end
      end

      it 'tracks the coverage test event using HLLRedisCounter' do
        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_2, values: user1.id)
          .and_call_original

        expect { track_artifact_report }.to change {
          counter.unique_events(event_names: test_event_name_2, start_date: start_time, end_date: end_time)
        }.by 1
      end
    end

    context 'when a single user started multiple pipelines with coverage reports' do
      let_it_be(:pipeline1) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
      let_it_be(:pipeline2) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }

      it 'tracks all pipelines using HLLRedisCounter by one user_id for the coverage test event' do
        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_2, values: user1.id)
          .twice
          .and_call_original

        expect do
          described_class.new.execute(pipeline1)
          described_class.new.execute(pipeline2)
        end.to change {
          counter.unique_events(event_names: test_event_name_2, start_date: start_time, end_date: end_time)
        }.by 1
      end
    end

    context 'when multiple users started multiple pipelines with coverage test reports' do
      let_it_be(:pipeline1) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
      let_it_be(:pipeline2) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user2) }

      it 'tracks all pipelines using HLLRedisCounter by multiple users for coverage test reports' do
        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_2, values: user1.id)
          .and_call_original

        expect(Gitlab::UsageDataCounters::HLLRedisCounter)
          .to receive(:track_event)
          .with(test_event_name_2, values: user2.id)
          .and_call_original

        expect do
          described_class.new.execute(pipeline1)
          described_class.new.execute(pipeline2)
        end.to change {
          counter.unique_events(event_names: test_event_name_2, start_date: start_time, end_date: end_time)
        }.by 2
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    class DestroyBatchService
      include BaseServiceUtility
      include ::Gitlab::Utils::StrongMemoize

      # Danger: Private - Should only be called in Ci Services that pass a batch of job artifacts
      # Not for use outside of the Ci:: namespace

      # Adds the passed batch of job artifacts to the `ci_deleted_objects` table
      # for asyncronous destruction of the objects in Object Storage via the `Ci::DeleteObjectsService`
      # and then deletes the batch of related `ci_job_artifacts` records.
      # Params:
      # +job_artifacts+:: A relation of job artifacts to destroy (fewer than MAX_JOB_ARTIFACT_BATCH_SIZE)
      # +pick_up_at+:: When to pick up for deletion of files
      # Returns:
      # +Hash+:: A hash with status and destroyed_artifacts_count keys
      def initialize(job_artifacts, pick_up_at: nil, skip_projects_on_refresh: false)
        @job_artifacts = job_artifacts.with_destroy_preloads.to_a
        @pick_up_at = pick_up_at
        @skip_projects_on_refresh = skip_projects_on_refresh
        @destroyed_ids = []
      end

      # rubocop: disable CodeReuse/ActiveRecord
      def execute(update_stats: true)
        if @skip_projects_on_refresh
          exclude_artifacts_undergoing_stats_refresh
        else
          track_artifacts_undergoing_stats_refresh
        end

        if @job_artifacts.empty?
          return success(destroyed_ids: @destroyed_ids, destroyed_artifacts_count: 0, statistics_updates: {})
        end

        destroy_related_records(@job_artifacts)

        destroy_around_hook(@job_artifacts) do
          @destroyed_ids = @job_artifacts.map(&:id)
          Ci::DeletedObject.transaction do
            Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at)
            Ci::JobArtifact.id_in(@destroyed_ids).delete_all
          end
        end

        after_batch_destroy_hook(@job_artifacts)

        update_project_statistics! if update_stats

        increment_monitoring_statistics(artifacts_count, artifacts_bytes)

        Gitlab::Ci::Artifacts::Logger.log_deleted(@job_artifacts, 'Ci::JobArtifacts::DestroyBatchService#execute')

        success(
          destroyed_ids: @destroyed_ids,
          destroyed_artifacts_count: artifacts_count,
          statistics_updates: statistics_updates_per_project
        )
      end
      # rubocop: enable CodeReuse/ActiveRecord

      private

      # Overriden in EE
      # :nocov:
      def destroy_around_hook(artifacts)
        yield
      end
      # :nocov:

      # Overriden in EE
      def destroy_related_records(artifacts); end

      # Overriden in EE
      def after_batch_destroy_hook(artifacts); end

      # using ! here since this can't be called inside a transaction
      def update_project_statistics!
        statistics_updates_per_project.each do |project, increments|
          ProjectStatistics.bulk_increment_statistic(project, Ci::JobArtifact.project_statistics_name, increments)
        end
      end

      def statistics_updates_per_project
        strong_memoize(:statistics_updates_per_project) do
          result = Hash.new { |updates, project| updates[project] = [] }

          @job_artifacts.each_with_object(result) do |job_artifact, result|
            increment = Gitlab::Counters::Increment.new(amount: -job_artifact.size.to_i, ref: job_artifact.id)
            result[job_artifact.project] << increment
          end
        end
      end

      def increment_monitoring_statistics(size, bytes)
        metrics.increment_destroyed_artifacts_count(size)
        metrics.increment_destroyed_artifacts_bytes(bytes)
      end

      def metrics
        @metrics ||= ::Gitlab::Ci::Artifacts::Metrics.new
      end

      def artifacts_count
        strong_memoize(:artifacts_count) do
          @job_artifacts.count
        end
      end

      def artifacts_bytes
        strong_memoize(:artifacts_bytes) do
          @job_artifacts.sum { |artifact| artifact.try(:size) || 0 }
        end
      end

      def track_artifacts_undergoing_stats_refresh
        project_ids = @job_artifacts.find_all do |artifact|
          artifact.project.refreshing_build_artifacts_size?
        end.map(&:project_id).uniq

        project_ids.each do |project_id|
          Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
            method: 'Ci::JobArtifacts::DestroyBatchService#execute',
            project_id: project_id
          )
        end
      end

      def exclude_artifacts_undergoing_stats_refresh
        project_ids = Set.new

        @job_artifacts.reject! do |artifact|
          next unless artifact.project.refreshing_build_artifacts_size?

          project_ids << artifact.project_id
        end

        if project_ids.any?
          Gitlab::ProjectStatsRefreshConflictsLogger.warn_skipped_artifact_deletion_during_stats_refresh(
            method: 'Ci::JobArtifacts::DestroyBatchService#execute',
            project_ids: project_ids
          )
        end
      end
    end
  end
end

Ci::JobArtifacts::DestroyBatchService.prepend_mod_with('Ci::JobArtifacts::DestroyBatchService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::DestroyBatchService, feature_category: :build_artifacts do
  let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id]) }
  let(:skip_projects_on_refresh) { false }
  let(:service) do
    described_class.new(
      artifacts,
      pick_up_at: Time.current,
      skip_projects_on_refresh: skip_projects_on_refresh
    )
  end

  let_it_be(:artifact_with_file, refind: true) do
    create(:ci_job_artifact, :zip)
  end

  let_it_be(:artifact_without_file, refind: true) do
    create(:ci_job_artifact)
  end

  let_it_be(:undeleted_artifact, refind: true) do
    create(:ci_job_artifact)
  end

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

    it 'creates a deleted object for artifact with attached file' do
      expect { subject }.to change { Ci::DeletedObject.count }.by(1)
    end

    it 'does not remove the attached file' do
      expect { execute }.not_to change { artifact_with_file.file.exists? }
    end

    it 'deletes the artifact records and logs them' do
      expect(Gitlab::Ci::Artifacts::Logger)
        .to receive(:log_deleted)
        .with(
          match_array([artifact_with_file, artifact_without_file]),
          'Ci::JobArtifacts::DestroyBatchService#execute'
        )

      expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
    end

    it 'reports metrics for destroyed artifacts' do
      expect_next_instance_of(Gitlab::Ci::Artifacts::Metrics) do |metrics|
        expect(metrics).to receive(:increment_destroyed_artifacts_count).with(2).and_call_original
        expect(metrics).to receive(:increment_destroyed_artifacts_bytes).with(107464).and_call_original
      end

      execute
    end

    context 'when artifact belongs to a project that is undergoing stats refresh' do
      let!(:artifact_under_refresh_1) do
        create(:ci_job_artifact, :zip)
      end

      let!(:artifact_under_refresh_2) do
        create(:ci_job_artifact, :zip)
      end

      let!(:artifact_under_refresh_3) do
        create(:ci_job_artifact, :zip, project: artifact_under_refresh_2.project)
      end

      let(:artifacts) do
        Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_under_refresh_1.id, artifact_under_refresh_2.id,
                                   artifact_under_refresh_3.id])
      end

      before do
        create(:project_build_artifacts_size_refresh, :created, project: artifact_with_file.project)
        create(:project_build_artifacts_size_refresh, :pending, project: artifact_under_refresh_1.project)
        create(:project_build_artifacts_size_refresh, :running, project: artifact_under_refresh_2.project)
      end

      shared_examples 'avoiding N+1 queries' do
        let!(:control_artifact_on_refresh) do
          create(:ci_job_artifact, :zip)
        end

        let!(:control_artifact_non_refresh) do
          create(:ci_job_artifact, :zip)
        end

        let!(:other_artifact_on_refresh) do
          create(:ci_job_artifact, :zip)
        end

        let!(:other_artifact_on_refresh_2) do
          create(:ci_job_artifact, :zip)
        end

        let!(:other_artifact_non_refresh) do
          create(:ci_job_artifact, :zip)
        end

        let!(:control_artifacts) do
          Ci::JobArtifact.where(
            id: [
              control_artifact_on_refresh.id,
              control_artifact_non_refresh.id
            ]
          )
        end

        let!(:artifacts) do
          Ci::JobArtifact.where(
            id: [
              other_artifact_on_refresh.id,
              other_artifact_on_refresh_2.id,
              other_artifact_non_refresh.id
            ]
          )
        end

        let(:control_service) do
          described_class.new(
            control_artifacts,
            pick_up_at: Time.current,
            skip_projects_on_refresh: skip_projects_on_refresh
          )
        end

        before do
          create(:project_build_artifacts_size_refresh, :pending, project: control_artifact_on_refresh.project)
          create(:project_build_artifacts_size_refresh, :pending, project: other_artifact_on_refresh.project)
          create(:project_build_artifacts_size_refresh, :pending, project: other_artifact_on_refresh_2.project)
        end

        it 'does not make multiple queries when fetching multiple project refresh records' do
          control = ActiveRecord::QueryRecorder.new { control_service.execute }

          expect { subject }.not_to exceed_query_limit(control)
        end
      end

      context 'and skip_projects_on_refresh is set to false (default)' do
        it 'logs the projects undergoing refresh and continues with the delete', :aggregate_failures do
          expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_artifact_deletion_during_stats_refresh).with(
            method: 'Ci::JobArtifacts::DestroyBatchService#execute',
            project_id: artifact_under_refresh_1.project.id
          ).once

          expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_artifact_deletion_during_stats_refresh).with(
            method: 'Ci::JobArtifacts::DestroyBatchService#execute',
            project_id: artifact_under_refresh_2.project.id
          ).once

          expect { subject }.to change { Ci::JobArtifact.count }.by(-4)
        end

        it_behaves_like 'avoiding N+1 queries'
      end

      context 'and skip_projects_on_refresh is set to true' do
        let(:skip_projects_on_refresh) { true }

        it 'logs the projects undergoing refresh and excludes the artifacts from deletion', :aggregate_failures do
          expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_skipped_artifact_deletion_during_stats_refresh).with(
            method: 'Ci::JobArtifacts::DestroyBatchService#execute',
            project_ids: match_array([artifact_under_refresh_1.project.id, artifact_under_refresh_2.project.id])
          )

          expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
          expect(Ci::JobArtifact.where(id: artifact_under_refresh_1.id)).to exist
          expect(Ci::JobArtifact.where(id: artifact_under_refresh_2.id)).to exist
          expect(Ci::JobArtifact.where(id: artifact_under_refresh_3.id)).to exist
        end

        it_behaves_like 'avoiding N+1 queries'
      end
    end

    context 'when artifact belongs to a project not undergoing refresh' do
      context 'and skip_projects_on_refresh is set to false (default)' do
        it 'does not log any warnings', :aggregate_failures do
          expect(Gitlab::ProjectStatsRefreshConflictsLogger).not_to receive(:warn_artifact_deletion_during_stats_refresh)

          expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
        end
      end

      context 'and skip_projects_on_refresh is set to true' do
        let(:skip_projects_on_refresh) { true }

        it 'does not log any warnings', :aggregate_failures do
          expect(Gitlab::ProjectStatsRefreshConflictsLogger).not_to receive(:warn_skipped_artifact_deletion_during_stats_refresh)

          expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
        end
      end
    end

    context 'ProjectStatistics', :sidekiq_inline do
      let_it_be(:project_1) { create(:project) }
      let_it_be(:project_2) { create(:project) }

      let(:artifact_with_file) { create(:ci_job_artifact, :zip, project: project_1) }
      let(:artifact_with_file_2) { create(:ci_job_artifact, :zip, project: project_1) }
      let(:artifact_without_file) { create(:ci_job_artifact, project: project_2) }
      let!(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id, artifact_with_file_2.id]) }

      it 'updates project statistics by the relevant amount' do
        expected_amount = -(artifact_with_file.size + artifact_with_file_2.size)

        expect { execute }
          .to change { project_1.statistics.reload.build_artifacts_size }.by(expected_amount)
          .and change { project_2.statistics.reload.build_artifacts_size }.by(0)
      end

      it 'increments project statistics with artifact size as amount and job artifact id as ref' do
        project_1_increments = [
          have_attributes(amount: -artifact_with_file.size, ref: artifact_with_file.id),
          have_attributes(amount: -artifact_with_file_2.file.size, ref: artifact_with_file_2.id)
        ]
        project_2_increments = [have_attributes(amount: 0, ref: artifact_without_file.id)]

        expect(ProjectStatistics).to receive(:bulk_increment_statistic).with(project_1, :build_artifacts_size, match_array(project_1_increments))
        expect(ProjectStatistics).to receive(:bulk_increment_statistic).with(project_2, :build_artifacts_size, match_array(project_2_increments))

        execute
      end

      context 'with update_stats: false' do
        subject(:execute) { service.execute(update_stats: false) }

        it 'does not update project statistics' do
          expect { execute }.not_to change { [project_1.statistics.reload.build_artifacts_size, project_2.statistics.reload.build_artifacts_size] }
        end

        it 'returns statistic updates per project' do
          project_1_updates = [
            have_attributes(amount: -artifact_with_file.size, ref: artifact_with_file.id),
            have_attributes(amount: -artifact_with_file_2.file.size, ref: artifact_with_file_2.id)
          ]
          project_2_updates = [have_attributes(amount: 0, ref: artifact_without_file.id)]

          expected_updates = {
            statistics_updates: {
              project_1 => match_array(project_1_updates),
              project_2 => project_2_updates
            }
          }

          expect(execute).to match(a_hash_including(expected_updates))
        end
      end
    end

    context 'when failed to destroy artifact' do
      context 'when the import fails' do
        before do
          expect(Ci::DeletedObject)
            .to receive(:bulk_import)
            .once
            .and_raise(ActiveRecord::RecordNotDestroyed)
        end

        it 'raises an exception and stop destroying' do
          expect { execute }.to raise_error(ActiveRecord::RecordNotDestroyed)
                            .and not_change { Ci::JobArtifact.count }
        end
      end
    end

    context 'when there are no artifacts' do
      let(:artifacts) { Ci::JobArtifact.none }

      it 'does not raise error' do
        expect { execute }.not_to raise_error
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq(destroyed_artifacts_count: 0, destroyed_ids: [], statistics_updates: {}, status: :success)
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    # This class is used by Ci::JobArtifact's FastDestroyAll implementation.
    # Ci::JobArtifact.begin_fast_destroy instantiates this service and calls #destroy_records.
    # This will set @statistics_updates instance variables.
    # The same instance is passed to Ci::JobArtifact.finalize_fast_destroy, which then calls
    # #update_statistics, using @statistics_updates set by #destroy_records.
    class DestroyAssociationsService
      BATCH_SIZE = 100

      def initialize(job_artifacts_relation)
        @job_artifacts_relation = job_artifacts_relation
        @statistics_updates = {}
      end

      def destroy_records
        @job_artifacts_relation.each_batch(of: BATCH_SIZE) do |relation|
          service = Ci::JobArtifacts::DestroyBatchService.new(relation, pick_up_at: Time.current)
          result  = service.execute(update_stats: false)
          @statistics_updates.merge!(result[:statistics_updates]) do |_project, existing_updates, new_updates|
            existing_updates.concat(new_updates)
          end
        end
      end

      def update_statistics
        @statistics_updates.each do |project, increments|
          ProjectStatistics.bulk_increment_statistic(project, Ci::JobArtifact.project_statistics_name, increments)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::DestroyAssociationsService, feature_category: :build_artifacts do
  let_it_be(:project_1) { create(:project) }
  let_it_be(:project_2) { create(:project) }

  let_it_be(:artifact_1, refind: true) { create(:ci_job_artifact, :zip, project: project_1) }
  let_it_be(:artifact_2, refind: true) { create(:ci_job_artifact, :junit, project: project_2) }
  let_it_be(:artifact_3, refind: true) { create(:ci_job_artifact, :terraform, project: project_1) }
  let_it_be(:artifact_4, refind: true) { create(:ci_job_artifact, :trace, project: project_2) }
  let_it_be(:artifact_5, refind: true) { create(:ci_job_artifact, :metadata, project: project_2) }

  let_it_be(:locked_artifact, refind: true) { create(:ci_job_artifact, :zip, :locked, project: project_1) }

  let(:artifact_ids_to_be_removed) { [artifact_1.id, artifact_2.id, artifact_3.id, artifact_4.id, artifact_5.id] }
  let(:artifacts) { Ci::JobArtifact.where(id: artifact_ids_to_be_removed) }
  let(:service) { described_class.new(artifacts) }

  describe '#destroy_records' do
    it 'removes all types of artifacts without updating statistics' do
      expect_next_instance_of(Ci::JobArtifacts::DestroyBatchService) do |service|
        expect(service).to receive(:execute).with(update_stats: false).and_call_original
      end

      expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count)
    end

    context 'with a locked artifact' do
      let(:artifact_ids_to_be_removed) { [artifact_1.id, locked_artifact.id] }

      it 'removes all artifacts' do
        expect { service.destroy_records }.to change { Ci::JobArtifact.count }.by(-artifact_ids_to_be_removed.count)
      end
    end

    context 'when there are no artifacts' do
      let(:artifacts) { Ci::JobArtifact.none }

      it 'does not raise error' do
        expect { service.destroy_records }.not_to raise_error
      end
    end
  end

  describe '#update_statistics' do
    before do
      stub_const("#{described_class}::BATCH_SIZE", 2)
      service.destroy_records
    end

    it 'updates project statistics' do
      project1_increments = [
        have_attributes(amount: -artifact_1.size, ref: artifact_1.id),
        have_attributes(amount: -artifact_3.size, ref: artifact_3.id)
      ]
      project2_increments = [
        have_attributes(amount: -artifact_2.size, ref: artifact_2.id),
        have_attributes(amount: -artifact_4.size, ref: artifact_4.id),
        have_attributes(amount: -artifact_5.size, ref: artifact_5.id)
      ]

      expect(ProjectStatistics).to receive(:bulk_increment_statistic).once
        .with(project_1, :build_artifacts_size, match_array(project1_increments))
      expect(ProjectStatistics).to receive(:bulk_increment_statistic).once
        .with(project_2, :build_artifacts_size, match_array(project2_increments))

      service.update_statistics
    end

    context 'when there are no artifacts' do
      let(:artifacts) { Ci::JobArtifact.none }

      it 'does not raise error' do
        expect { service.update_statistics }.not_to raise_error
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    class DestroyAllExpiredService
      include ::Gitlab::ExclusiveLeaseHelpers
      include ::Gitlab::LoopHelpers

      BATCH_SIZE = 100
      LOOP_LIMIT = 500
      LOOP_TIMEOUT = 5.minutes
      LOCK_TIMEOUT = 6.minutes
      EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock'

      def initialize
        @removed_artifacts_count = 0
        @start_at = Time.current
      end

      ##
      # Destroy expired job artifacts on GitLab instance
      #
      # This destroy process cannot run for more than 6 minutes. This is for
      # preventing multiple `ExpireBuildArtifactsWorker` CRON jobs run concurrently,
      # which is scheduled every 7 minutes.
      def execute
        in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
          destroy_unlocked_job_artifacts
        end

        @removed_artifacts_count
      end

      private

      def destroy_unlocked_job_artifacts
        loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
          artifacts = Ci::JobArtifact.expired_before(@start_at).non_trace.artifact_unlocked.limit(BATCH_SIZE)
          service_response = destroy_batch(artifacts)
          @removed_artifacts_count += service_response[:destroyed_artifacts_count]
        end
      end

      def destroy_batch(artifacts)
        Ci::JobArtifacts::DestroyBatchService.new(artifacts, skip_projects_on_refresh: true).execute
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state,
  feature_category: :build_artifacts do
  include ExclusiveLeaseHelpers

  let(:service) { described_class.new }

  describe '.execute' do
    subject { service.execute }

    let_it_be(:locked_pipeline) { create(:ci_pipeline, :artifacts_locked) }
    let_it_be(:pipeline) { create(:ci_pipeline, :unlocked) }
    let_it_be(:locked_job) { create(:ci_build, :success, pipeline: locked_pipeline) }
    let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline) }

    context 'when artifact is expired' do
      let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }

      context 'with preloaded relationships' do
        let(:second_artifact) { create(:ci_job_artifact, :expired, :junit, job: job) }

        let(:more_artifacts) do
          [
            create(:ci_job_artifact, :expired, :sast, job: job),
            create(:ci_job_artifact, :expired, :metadata, job: job),
            create(:ci_job_artifact, :expired, :codequality, job: job),
            create(:ci_job_artifact, :expired, :accessibility, job: job)
          ]
        end

        before do
          stub_const("#{described_class}::LOOP_LIMIT", 1)

          # This artifact-with-file is created before the control execution to ensure
          # that the DeletedObject operations are accounted for in the query count.
          second_artifact
        end

        it 'performs a consistent number of queries' do
          control = ActiveRecord::QueryRecorder.new { service.execute }

          more_artifacts

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

      context 'when artifact is not locked' do
        it 'deletes job artifact record' do
          expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
        end

        context 'when the artifact does not have a file attached to it' do
          it 'does not create deleted objects' do
            expect(artifact.exists?).to be_falsy # sanity check

            expect { subject }.not_to change { Ci::DeletedObject.count }
          end
        end

        context 'when the artifact has a file attached to it' do
          let!(:artifact) { create(:ci_job_artifact, :expired, :zip, job: job, locked: job.pipeline.locked) }

          it 'creates a deleted object' do
            expect { subject }.to change { Ci::DeletedObject.count }.by(1)
          end

          it 'resets project statistics', :sidekiq_inline do
            expect { subject }
              .to change { artifact.project.statistics.reload.build_artifacts_size }.by(-artifact.file.size)
          end

          it 'does not remove the files' do
            expect { subject }.not_to change { artifact.file.exists? }
          end
        end

        context 'when the project in which the artifact belongs to is undergoing stats refresh' do
          before do
            create(:project_build_artifacts_size_refresh, :pending, project: artifact.project)
          end

          it 'does not destroy job artifact' do
            expect { subject }.not_to change { Ci::JobArtifact.count }
          end
        end
      end

      context 'when artifact is locked' do
        let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }

        it 'does not destroy job artifact' do
          expect { subject }.not_to change { Ci::JobArtifact.count }
        end
      end
    end

    context 'when artifact is not expired' do
      let!(:artifact) { create(:ci_job_artifact, job: job, locked: job.pipeline.locked) }

      it 'does not destroy expired job artifacts' do
        expect { subject }.not_to change { Ci::JobArtifact.count }
      end
    end

    context 'when artifact is permanent' do
      let!(:artifact) { create(:ci_job_artifact, expire_at: nil, job: job, locked: job.pipeline.locked) }

      it 'does not destroy expired job artifacts' do
        expect { subject }.not_to change { Ci::JobArtifact.count }
      end
    end

    context 'when failed to destroy artifact' do
      let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }

      before do
        stub_const("#{described_class}::LOOP_LIMIT", 10)
      end

      context 'when the import fails' do
        before do
          expect(Ci::DeletedObject)
            .to receive(:bulk_import)
            .once
            .and_raise(ActiveRecord::RecordNotDestroyed)
        end

        it 'raises an exception and stop destroying' do
          expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed)
                            .and not_change { Ci::JobArtifact.count }.from(1)
        end
      end

      context 'when the delete fails' do
        before do
          expect(Ci::JobArtifact)
            .to receive(:id_in)
            .once
            .and_raise(ActiveRecord::RecordNotDestroyed)
        end

        it 'raises an exception rolls back the insert' do
          expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed)
                            .and not_change { Ci::DeletedObject.count }.from(0)
        end
      end
    end

    context 'when exclusive lease has already been taken by the other instance' do
      let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }

      before do
        stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
      end

      it 'raises an error and does not start destroying' do
        expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
                          .and not_change { Ci::JobArtifact.count }.from(1)
      end
    end

    context 'with a second artifact and batch size of 1' do
      let(:second_job) { create(:ci_build, :success, pipeline: pipeline) }
      let!(:second_artifact) { create(:ci_job_artifact, :archive, expire_at: 1.day.ago, job: second_job, locked: job.pipeline.locked) }
      let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }

      before do
        stub_const("#{described_class}::BATCH_SIZE", 1)
      end

      context 'when timeout happens' do
        before do
          stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
        end

        it 'destroys one artifact' do
          expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
        end

        it 'reports the number of destroyed artifacts' do
          is_expected.to eq(1)
        end
      end

      context 'when loop reached loop limit' do
        before do
          stub_const("#{described_class}::LOOP_LIMIT", 1)
        end

        it 'destroys one artifact' do
          expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
        end

        it 'reports the number of destroyed artifacts' do
          is_expected.to eq(1)
        end
      end

      context 'when the number of artifacts is greater than than batch size' do
        it 'destroys all expired artifacts' do
          expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
        end

        it 'reports the number of destroyed artifacts' do
          is_expected.to eq(2)
        end
      end
    end

    context 'when there are no artifacts' do
      it 'does not raise error' do
        expect { subject }.not_to raise_error
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq(0)
      end
    end

    context 'when some artifacts are locked' do
      let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
      let!(:locked_artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }

      it 'destroys only unlocked artifacts' do
        expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
        expect(locked_artifact).to be_persisted
      end
    end

    context 'when some artifacts are trace' do
      let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
      let!(:trace_artifact) { create(:ci_job_artifact, :trace, :expired, job: job, locked: job.pipeline.locked) }

      it 'destroys only non trace artifacts' do
        expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
        expect(trace_artifact).to be_persisted
      end
    end

    context 'when all artifacts are locked' do
      let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }

      it 'destroys no artifacts' do
        expect { subject }.to change { Ci::JobArtifact.count }.by(0)
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    class UpdateUnknownLockedStatusService
      include ::Gitlab::ExclusiveLeaseHelpers
      include ::Gitlab::LoopHelpers

      BATCH_SIZE = 100
      LOOP_TIMEOUT = 5.minutes
      LOOP_LIMIT = 100
      LARGE_LOOP_LIMIT = 500
      EXCLUSIVE_LOCK_KEY = 'unknown_status_job_artifacts:update:lock'
      LOCK_TIMEOUT = 6.minutes

      def initialize
        @removed_count = 0
        @locked_count = 0
        @start_at = Time.current
        @loop_limit = Feature.enabled?(:ci_job_artifacts_backlog_large_loop_limit) ? LARGE_LOOP_LIMIT : LOOP_LIMIT
      end

      def execute
        in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
          update_locked_status_on_unknown_artifacts
        end

        { removed: @removed_count, locked: @locked_count }
      end

      private

      def update_locked_status_on_unknown_artifacts
        loop_until(timeout: LOOP_TIMEOUT, limit: @loop_limit) do
          unknown_status_build_ids = safely_ordered_ci_job_artifacts_locked_unknown_relation.pluck_job_id.uniq

          locked_pipe_build_ids = ::Ci::Build
            .with_pipeline_locked_artifacts
            .id_in(unknown_status_build_ids)
            .pluck_primary_key

          @locked_count += update_unknown_artifacts(locked_pipe_build_ids, Ci::JobArtifact.lockeds[:artifacts_locked])

          unlocked_pipe_build_ids = unknown_status_build_ids - locked_pipe_build_ids
          service_response = batch_destroy_artifacts(unlocked_pipe_build_ids)
          @removed_count += service_response[:destroyed_artifacts_count]
        end
      end

      def update_unknown_artifacts(build_ids, locked_value)
        return 0 unless build_ids.any?

        expired_locked_unknown_artifacts.for_job_ids(build_ids).update_all(locked: locked_value)
      end

      def batch_destroy_artifacts(build_ids)
        deleteable_artifacts_relation =
          if build_ids.any?
            expired_locked_unknown_artifacts.for_job_ids(build_ids)
          else
            Ci::JobArtifact.none
          end

        Ci::JobArtifacts::DestroyBatchService.new(deleteable_artifacts_relation).execute
      end

      def expired_locked_unknown_artifacts
        # UPDATE queries perform better without the specific order and limit
        # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76509#note_891260455
        Ci::JobArtifact.expired_before(@start_at).artifact_unknown
      end

      def safely_ordered_ci_job_artifacts_locked_unknown_relation
        # Adding the ORDER and LIMIT improves performance when we don't have build_id
        expired_locked_unknown_artifacts.limit(BATCH_SIZE).order_expired_asc
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::UpdateUnknownLockedStatusService, :clean_gitlab_redis_shared_state,
  feature_category: :build_artifacts do
  include ExclusiveLeaseHelpers

  let(:service) { described_class.new }

  describe '.execute' do
    subject { service.execute }

    let_it_be(:locked_pipeline) { create(:ci_pipeline, :artifacts_locked) }
    let_it_be(:pipeline) { create(:ci_pipeline, :unlocked) }
    let_it_be(:locked_job) { create(:ci_build, :success, pipeline: locked_pipeline) }
    let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline) }

    let!(:unknown_unlocked_artifact) do
      create(:ci_job_artifact, :junit, expire_at: 1.hour.ago, job: job, locked: Ci::JobArtifact.lockeds[:unknown])
    end

    let!(:unknown_locked_artifact) do
      create(:ci_job_artifact, :lsif,
        expire_at: 1.day.ago,
        job: locked_job,
        locked: Ci::JobArtifact.lockeds[:unknown]
      )
    end

    let!(:unlocked_artifact) do
      create(:ci_job_artifact, :archive, expire_at: 1.hour.ago, job: job, locked: Ci::JobArtifact.lockeds[:unlocked])
    end

    let!(:locked_artifact) do
      create(:ci_job_artifact, :sast, :raw,
        expire_at: 1.day.ago,
        job: locked_job,
        locked: Ci::JobArtifact.lockeds[:artifacts_locked]
      )
    end

    context 'when artifacts are expired' do
      it 'sets artifact_locked when the pipeline is locked' do
        expect { service.execute }
          .to change { unknown_locked_artifact.reload.locked }.from('unknown').to('artifacts_locked')
          .and not_change { Ci::JobArtifact.exists?(locked_artifact.id) }
      end

      it 'destroys the artifact when the pipeline is unlocked' do
        expect { subject }.to change { Ci::JobArtifact.exists?(unknown_unlocked_artifact.id) }.from(true).to(false)
      end

      it 'does not update ci_job_artifact rows with known locked values' do
        expect { service.execute }
          .to not_change(locked_artifact, :attributes)
          .and not_change { Ci::JobArtifact.exists?(locked_artifact.id) }
          .and not_change(unlocked_artifact, :attributes)
          .and not_change { Ci::JobArtifact.exists?(unlocked_artifact.id) }
      end

      it 'logs the counts of affected artifacts' do
        expect(subject).to eq({ removed: 1, locked: 1 })
      end
    end

    context 'in a single iteration' do
      before do
        stub_const("#{described_class}::BATCH_SIZE", 1)
      end

      context 'due to the LOOP_TIMEOUT' do
        before do
          stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
        end

        it 'affects the earliest expired artifact first' do
          subject

          expect(unknown_locked_artifact.reload.locked).to eq('artifacts_locked')
          expect(unknown_unlocked_artifact.reload.locked).to eq('unknown')
        end

        it 'reports the number of destroyed artifacts' do
          is_expected.to eq({ removed: 0, locked: 1 })
        end
      end

      context 'due to @loop_limit' do
        before do
          stub_const("#{described_class}::LARGE_LOOP_LIMIT", 1)
        end

        it 'affects the most recently expired artifact first' do
          subject

          expect(unknown_locked_artifact.reload.locked).to eq('artifacts_locked')
          expect(unknown_unlocked_artifact.reload.locked).to eq('unknown')
        end

        it 'reports the number of destroyed artifacts' do
          is_expected.to eq({ removed: 0, locked: 1 })
        end
      end
    end

    context 'when artifact is not expired' do
      let!(:unknown_unlocked_artifact) do
        create(:ci_job_artifact, :junit,
          expire_at: 1.year.from_now,
          job: job,
          locked: Ci::JobArtifact.lockeds[:unknown]
        )
      end

      it 'does not change the locked status' do
        expect { service.execute }.not_to change { unknown_unlocked_artifact.locked }
        expect(Ci::JobArtifact.exists?(unknown_unlocked_artifact.id)).to eq(true)
      end
    end

    context 'when exclusive lease has already been taken by the other instance' do
      before do
        stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
      end

      it 'raises an error and' do
        expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
      end
    end

    context 'when there are no unknown status artifacts' do
      before do
        Ci::JobArtifact.update_all(locked: :unlocked)
      end

      it 'does not raise error' do
        expect { subject }.not_to raise_error
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq({ removed: 0, locked: 0 })
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    class BulkDeleteByProjectService
      include BaseServiceUtility

      JOB_ARTIFACTS_COUNT_LIMIT = 50

      def initialize(job_artifact_ids:, project:, current_user:)
        @job_artifact_ids = job_artifact_ids
        @project = project
        @current_user = current_user
      end

      def execute
        if exceeds_limits?
          return ServiceResponse.error(
            message: "Can only delete up to #{JOB_ARTIFACTS_COUNT_LIMIT} job artifacts per call"
          )
        end

        find_result = find_artifacts

        return ServiceResponse.error(message: find_result[:error_message]) if find_result[:error_message]

        @job_artifact_scope = find_result[:scope]

        unless all_job_artifacts_belong_to_project?
          return ServiceResponse.error(message: 'Not all artifacts belong to requested project')
        end

        result = Ci::JobArtifacts::DestroyBatchService.new(job_artifact_scope).execute

        destroyed_artifacts_count = result.fetch(:destroyed_artifacts_count)
        destroyed_ids = result.fetch(:destroyed_ids)

        ServiceResponse.success(
          payload: {
            destroyed_count: destroyed_artifacts_count,
            destroyed_ids: destroyed_ids,
            errors: []
          })
      end

      private

      def find_artifacts
        job_artifacts = ::Ci::JobArtifact.id_in(job_artifact_ids)

        error_message = nil
        if job_artifacts.count != job_artifact_ids.count
          not_found_artifacts = job_artifact_ids - job_artifacts.map(&:id)
          error_message = "Artifacts (#{not_found_artifacts.join(',')}) not found"
        end

        { scope: job_artifacts, error_message: error_message }
      end

      def exceeds_limits?
        job_artifact_ids.count > JOB_ARTIFACTS_COUNT_LIMIT
      end

      def all_job_artifacts_belong_to_project?
        # rubocop:disable CodeReuse/ActiveRecord
        job_artifact_scope.pluck(:project_id).all?(project.id)
        # rubocop:enable CodeReuse/ActiveRecord
      end

      attr_reader :job_artifact_ids, :job_artifact_scope, :current_user, :project
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::JobArtifacts::BulkDeleteByProjectService, "#execute", feature_category: :build_artifacts do
  subject(:execute) do
    described_class.new(
      job_artifact_ids: job_artifact_ids,
      current_user: current_user,
      project: project).execute
  end

  let_it_be(:current_user) { create(:user) }
  let_it_be(:build, reload: true) do
    create(:ci_build, :artifacts, :trace_artifact, user: current_user)
  end

  let_it_be(:project) { build.project }
  let_it_be(:job_artifact_ids) { build.job_artifacts.map(&:id) }

  describe '#execute' do
    context 'when number of artifacts exceeds limits to delete' do
      let_it_be(:second_build, reload: true) do
        create(:ci_build, :artifacts, :trace_artifact, user: current_user, project: project)
      end

      let_it_be(:job_artifact_ids) { ::Ci::JobArtifact.all.map(&:id) }

      before do
        project.add_maintainer(current_user)
        stub_const("#{described_class}::JOB_ARTIFACTS_COUNT_LIMIT", 1)
      end

      it 'fails to destroy' do
        result = execute

        expect(result).to be_error
        expect(result[:message]).to eq('Can only delete up to 1 job artifacts per call')
      end
    end

    context 'when requested not existing artifacts do delete' do
      let_it_be(:deleted_build, reload: true) do
        create(:ci_build, :artifacts, :trace_artifact, user: current_user, project: project)
      end

      let_it_be(:deleted_job_artifacts) { deleted_build.job_artifacts }
      let_it_be(:job_artifact_ids) { ::Ci::JobArtifact.all.map(&:id) }

      before do
        project.add_maintainer(current_user)
        deleted_job_artifacts.each(&:destroy!)
      end

      it 'fails to destroy' do
        result = execute

        expect(result).to be_error
        expect(result[:message]).to eq("Artifacts (#{deleted_job_artifacts.map(&:id).join(',')}) not found")
      end
    end

    context 'when maintainer has access to the project' do
      before do
        project.add_maintainer(current_user)
      end

      it 'is successful' do
        result = execute

        expect(result).to be_success
        expect(result.payload).to eq(
          {
            destroyed_count: job_artifact_ids.count,
            destroyed_ids: job_artifact_ids,
            errors: []
          }
        )
        expect(::Ci::JobArtifact.where(id: job_artifact_ids).count).to eq(0)
      end

      context 'and partially owns artifacts' do
        let_it_be(:orphan_artifact) { create(:ci_job_artifact, :archive) }
        let_it_be(:orphan_artifact_id) { orphan_artifact.id }
        let_it_be(:owned_artifacts_ids) { build.job_artifacts.erasable.map(&:id) }
        let_it_be(:job_artifact_ids) { [orphan_artifact_id] + owned_artifacts_ids }

        it 'fails to destroy' do
          result = execute

          expect(result).to be_error
          expect(result[:message]).to be('Not all artifacts belong to requested project')
          expect(::Ci::JobArtifact.where(id: job_artifact_ids).count).to eq(3)
        end
      end

      context 'and request all artifacts from a different project' do
        let_it_be(:different_project_artifact) { create(:ci_job_artifact, :archive) }
        let_it_be(:job_artifact_ids) { [different_project_artifact] }

        let_it_be(:different_build, reload: true) do
          create(:ci_build, :artifacts, :trace_artifact, user: current_user)
        end

        let_it_be(:different_project) { different_build.project }

        before do
          different_project.add_maintainer(current_user)
        end

        it 'returns a error' do
          result = execute

          expect(result).to be_error
          expect(result[:message]).to be('Not all artifacts belong to requested project')
          expect(::Ci::JobArtifact.where(id: job_artifact_ids).count).to eq(job_artifact_ids.count)
        end
      end
    end
  end
end