project_pipeline_status_spec.rb 10.2 KB
Newer Older
1 2
require 'spec_helper'

3
describe Gitlab::Cache::Ci::ProjectPipelineStatus, :redis do
4
  let!(:project) { create(:project) }
5
  let(:pipeline_status) { described_class.new(project) }
6
  let(:cache_key) { "projects/#{project.id}/pipeline_status" }
7 8 9 10 11 12 13 14 15

  describe '.load_for_project' do
    it "loads the status" do
      expect_any_instance_of(described_class).to receive(:load_status)

      described_class.load_for_project(project)
    end
  end

16 17 18 19 20
  describe 'loading in batches' do
    let(:status) { 'success' }
    let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
    let(:ref) { 'master' }
    let(:pipeline_info) { { sha: sha, status: status, ref: ref } }
21
    let!(:project_without_status) { create(:project) }
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109

    describe '.load_in_batch_for_projects' do
      it 'preloads pipeline_status on projects' do
        described_class.load_in_batch_for_projects([project])

        # Don't call the accessor that would lazy load the variable
        expect(project.instance_variable_get('@pipeline_status')).to be_a(described_class)
      end

      describe 'without a status in redis' do
        it 'loads the status from a commit when it was not in redis' do
          empty_status = { sha: nil, status: nil, ref: nil }
          fake_pipeline = described_class.new(
            project_without_status,
            pipeline_info: empty_status,
            loaded_from_cache: false
          )

          expect(described_class).to receive(:new).
                                       with(project_without_status,
                                            pipeline_info: empty_status,
                                            loaded_from_cache: false).
                                       and_return(fake_pipeline)
          expect(fake_pipeline).to receive(:load_from_project)
          expect(fake_pipeline).to receive(:store_in_cache)

          described_class.load_in_batch_for_projects([project_without_status])
        end

        it 'only connects to redis twice' do
          # Once to load, once to store in the cache
          expect(Gitlab::Redis).to receive(:with).exactly(2).and_call_original

          described_class.load_in_batch_for_projects([project_without_status])

          expect(project_without_status.pipeline_status).not_to be_nil
        end
      end

      describe 'when a status was cached in redis' do
        before do
          Gitlab::Redis.with do |redis|
            redis.mapped_hmset(cache_key,
                               { sha: sha, status: status, ref: ref })
          end
        end

        it 'loads the correct status' do
          described_class.load_in_batch_for_projects([project])

          pipeline_status = project.instance_variable_get('@pipeline_status')

          expect(pipeline_status.sha).to eq(sha)
          expect(pipeline_status.status).to eq(status)
          expect(pipeline_status.ref).to eq(ref)
        end

        it 'only connects to redis once' do
          expect(Gitlab::Redis).to receive(:with).exactly(1).and_call_original

          described_class.load_in_batch_for_projects([project])

          expect(project.pipeline_status).not_to be_nil
        end

        it "doesn't load the status separatly" do
          expect_any_instance_of(described_class).not_to receive(:load_from_project)
          expect_any_instance_of(described_class).not_to receive(:load_from_cache)

          described_class.load_in_batch_for_projects([project])
        end
      end
    end

    describe '.cached_results_for_projects' do
      it 'loads a status from redis for all projects' do
        Gitlab::Redis.with do |redis|
          redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
        end

        result = [{ loaded_from_cache: false, pipeline_info: { sha: nil, status: nil, ref: nil } },
                  { loaded_from_cache: true, pipeline_info: pipeline_info }]

        expect(described_class.cached_results_for_projects([project_without_status, project])).to eq(result)
      end
    end
  end

110 111
  describe '.update_for_pipeline' do
    it 'refreshes the cache if nescessary' do
112 113
      pipeline = build_stubbed(:ci_pipeline,
                               sha: '123456', status: 'success', ref: 'master')
114 115
      fake_status = double
      expect(described_class).to receive(:new).
116 117 118 119
                                   with(pipeline.project,
                                        pipeline_info: {
                                          sha: '123456', status: 'success', ref: 'master'
                                        }).
120 121 122 123 124 125 126 127
                                   and_return(fake_status)

      expect(fake_status).to receive(:store_in_cache_if_needed)

      described_class.update_for_pipeline(pipeline)
    end
  end

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
  describe '#has_status?' do
    it "is false when the status wasn't loaded yet" do
      expect(pipeline_status.has_status?).to be_falsy
    end

    it 'is true when all status information was loaded' do
      fake_commit = double
      allow(fake_commit).to receive(:status).and_return('failed')
      allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6')
      allow(pipeline_status).to receive(:commit).and_return(fake_commit)
      allow(pipeline_status).to receive(:has_cache?).and_return(false)

      pipeline_status.load_status

      expect(pipeline_status.has_status?).to be_truthy
    end
  end

  describe '#load_status' do
    it 'loads the status from the cache when there is one' do
      expect(pipeline_status).to receive(:has_cache?).and_return(true)
      expect(pipeline_status).to receive(:load_from_cache)

      pipeline_status.load_status
    end

    it 'loads the status from the project commit when there is no cache' do
      allow(pipeline_status).to receive(:has_cache?).and_return(false)

157
      expect(pipeline_status).to receive(:load_from_project)
158 159 160 161 162 163

      pipeline_status.load_status
    end

    it 'stores the status in the cache when it loading it from the project' do
      allow(pipeline_status).to receive(:has_cache?).and_return(false)
164
      allow(pipeline_status).to receive(:load_from_project)
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185

      expect(pipeline_status).to receive(:store_in_cache)

      pipeline_status.load_status
    end

    it 'sets the state to loaded' do
      pipeline_status.load_status

      expect(pipeline_status).to be_loaded
    end

    it 'only loads the status once' do
      expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1)
      expect(pipeline_status).to receive(:load_from_cache).exactly(1)

      pipeline_status.load_status
      pipeline_status.load_status
    end
  end

186
  describe "#load_from_project" do
187 188 189
    let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }

    it 'reads the status from the pipeline for the commit' do
190
      pipeline_status.load_from_project
191 192 193

      expect(pipeline_status.status).to eq('success')
      expect(pipeline_status.sha).to eq(project.commit.sha)
194
      expect(pipeline_status.ref).to eq(project.default_branch)
195
    end
196 197 198 199 200 201 202 203

    it "doesn't fail for an empty project" do
      status_for_empty_commit = described_class.new(create(:empty_project))

      status_for_empty_commit.load_status

      expect(status_for_empty_commit).to be_loaded
    end
204 205 206 207 208 209 210 211
  end

  describe "#store_in_cache", :redis do
    it "sets the object in redis" do
      pipeline_status.sha = '123456'
      pipeline_status.status = 'failed'

      pipeline_status.store_in_cache
212
      read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) }
213 214 215 216 217 218 219 220 221

      expect(read_sha).to eq('123456')
      expect(read_status).to eq('failed')
    end
  end

  describe '#store_in_cache_if_needed', :redis do
    it 'stores the state in the cache when the sha is the HEAD of the project' do
      create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
222
      pipeline_status = described_class.load_for_project(project)
223

224 225
      pipeline_status.store_in_cache_if_needed
      sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status, :ref) }
226 227 228

      expect(sha).not_to be_nil
      expect(status).not_to be_nil
229
      expect(ref).not_to be_nil
230 231 232
    end

    it "doesn't store the status in redis when the sha is not the head of the project" do
233 234 235 236
      other_status = described_class.new(
        project,
        pipeline_info: { sha: "123456", status: "failed" }
      )
237 238

      other_status.store_in_cache_if_needed
239
      sha, status = Gitlab::Redis.with { |redis| redis.hmget(cache_key, :sha, :status) }
240 241 242 243 244 245 246

      expect(sha).to be_nil
      expect(status).to be_nil
    end

    it "deletes the cache if the repository doesn't have a head commit" do
      empty_project = create(:empty_project)
247 248 249 250 251 252 253 254 255
      Gitlab::Redis.with do |redis|
        redis.mapped_hmset(cache_key,
                           { sha: 'sha', status: 'pending', ref: 'master' })
      end

      other_status = described_class.new(empty_project,
                                         pipeline_info: {
                                           sha: "123456", status: "failed"
                                         })
256 257

      other_status.store_in_cache_if_needed
258
      sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/pipeline_status", :sha, :status, :ref) }
259 260 261

      expect(sha).to be_nil
      expect(status).to be_nil
262
      expect(ref).to be_nil
263 264 265 266 267 268
    end
  end

  describe "with a status in redis", :redis do
    let(:status) { 'success' }
    let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
269
    let(:ref) { 'master' }
270 271

    before do
272 273 274 275
      Gitlab::Redis.with do |redis|
        redis.mapped_hmset(cache_key,
                           { sha: sha, status: status, ref: ref })
      end
276 277 278 279 280 281 282 283
    end

    describe '#load_from_cache' do
      it 'reads the status from redis' do
        pipeline_status.load_from_cache

        expect(pipeline_status.sha).to eq(sha)
        expect(pipeline_status.status).to eq(status)
284
        expect(pipeline_status.ref).to eq(ref)
285 286 287 288 289 290 291 292 293 294 295 296 297
      end
    end

    describe '#has_cache?' do
      it 'knows the status is cached' do
        expect(pipeline_status.has_cache?).to be_truthy
      end
    end

    describe '#delete_from_cache' do
      it 'deletes values from redis'  do
        pipeline_status.delete_from_cache

298
        key_exists = Gitlab::Redis.with { |redis| redis.exists(cache_key) }
299 300 301 302 303 304

        expect(key_exists).to be_falsy
      end
    end
  end
end