2019-07-25 05:21:37 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2017-03-14 09:05:45 +00:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2020-06-24 09:08:32 +00:00
|
|
|
RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
|
2020-07-15 12:09:26 +00:00
|
|
|
let_it_be(:project) { create(:project, :repository) }
|
2017-03-14 09:05:45 +00:00
|
|
|
let(:pipeline_status) { described_class.new(project) }
|
2018-11-23 08:41:08 +00:00
|
|
|
let(:cache_key) { pipeline_status.cache_key }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
describe '.load_for_project' do
|
|
|
|
it "loads the status" do
|
2019-12-07 00:07:51 +00:00
|
|
|
expect_next_instance_of(described_class) do |instance|
|
|
|
|
expect(instance).to receive(:load_status)
|
|
|
|
end
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
described_class.load_for_project(project)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-26 12:04:22 +00:00
|
|
|
describe 'loading in batches' do
|
|
|
|
describe '.load_in_batch_for_projects' do
|
2018-11-23 08:41:08 +00:00
|
|
|
it 'loads pipeline_status on projects' do
|
2017-04-26 12:04:22 +00:00
|
|
|
described_class.load_in_batch_for_projects([project])
|
|
|
|
|
|
|
|
# Don't call the accessor that would lazy load the variable
|
2018-11-23 08:41:08 +00:00
|
|
|
project_pipeline_status = project.instance_variable_get('@pipeline_status')
|
2017-04-26 12:04:22 +00:00
|
|
|
|
2018-11-23 08:41:08 +00:00
|
|
|
expect(project_pipeline_status).to be_a(described_class)
|
|
|
|
expect(project_pipeline_status).to be_loaded
|
2017-04-26 12:04:22 +00:00
|
|
|
end
|
|
|
|
|
2018-11-23 08:41:08 +00:00
|
|
|
it 'loads 10 projects without hitting Gitaly call limit', :request_store do
|
|
|
|
projects = Gitlab::GitalyClient.allow_n_plus_1_calls do
|
|
|
|
(1..10).map { create(:project, :repository) }
|
2017-04-26 12:04:22 +00:00
|
|
|
end
|
2018-11-23 08:41:08 +00:00
|
|
|
Gitlab::GitalyClient.reset_counts
|
2017-04-26 12:04:22 +00:00
|
|
|
|
2018-11-23 08:41:08 +00:00
|
|
|
expect { described_class.load_in_batch_for_projects(projects) }.not_to raise_error
|
2017-04-26 12:04:22 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-03-31 13:47:34 +00:00
|
|
|
describe '.update_for_pipeline' do
|
|
|
|
it 'refreshes the cache if nescessary' do
|
2017-04-26 12:04:22 +00:00
|
|
|
pipeline = build_stubbed(:ci_pipeline,
|
|
|
|
sha: '123456', status: 'success', ref: 'master')
|
2017-03-31 13:47:34 +00:00
|
|
|
fake_status = double
|
2017-06-21 13:48:12 +00:00
|
|
|
expect(described_class).to receive(:new)
|
|
|
|
.with(pipeline.project,
|
2017-04-26 12:04:22 +00:00
|
|
|
pipeline_info: {
|
|
|
|
sha: '123456', status: 'success', ref: 'master'
|
2017-06-21 13:48:12 +00:00
|
|
|
})
|
|
|
|
.and_return(fake_status)
|
2017-03-31 13:47:34 +00:00
|
|
|
|
|
|
|
expect(fake_status).to receive(:store_in_cache_if_needed)
|
|
|
|
|
|
|
|
described_class.update_for_pipeline(pipeline)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-03-14 09:05:45 +00:00
|
|
|
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
|
2020-07-15 12:09:26 +00:00
|
|
|
describe 'gitaly call counts', :request_store do
|
|
|
|
context 'not cached' do
|
|
|
|
before do
|
|
|
|
expect(pipeline_status).not_to be_has_cache
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'ci_pipeline_status_omit_commit_sha_in_cache_key is enabled' do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: project)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'makes a Gitaly call' do
|
|
|
|
expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'ci_pipeline_status_omit_commit_sha_in_cache_key is disabled' do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'makes a Gitaly calls' do
|
|
|
|
expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'cached' do
|
|
|
|
before do
|
|
|
|
described_class.load_in_batch_for_projects([project])
|
|
|
|
|
|
|
|
expect(pipeline_status).to be_has_cache
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'ci_pipeline_status_omit_commit_sha_in_cache_key is enabled' do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: project)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'makes no Gitaly calls' do
|
|
|
|
expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(0)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'ci_pipeline_status_omit_commit_sha_in_cache_key is disabled' do
|
|
|
|
before do
|
|
|
|
stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'makes a Gitaly calls' do
|
|
|
|
expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-03-14 09:05:45 +00:00
|
|
|
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)
|
|
|
|
|
2017-04-06 12:20:29 +00:00
|
|
|
expect(pipeline_status).to receive(:load_from_project)
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
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)
|
2017-04-06 12:20:29 +00:00
|
|
|
allow(pipeline_status).to receive(:load_from_project)
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
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
|
2020-01-28 21:08:56 +00:00
|
|
|
|
|
|
|
it 'handles Gitaly unavailable exceptions gracefully' do
|
|
|
|
allow(pipeline_status).to receive(:commit).and_raise(GRPC::Unavailable)
|
|
|
|
|
|
|
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
|
|
|
|
an_instance_of(GRPC::Unavailable), project_id: project.id
|
|
|
|
)
|
|
|
|
expect { pipeline_status.load_status }.not_to raise_error
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'handles Gitaly timeout exceptions gracefully' do
|
|
|
|
allow(pipeline_status).to receive(:commit).and_raise(GRPC::DeadlineExceeded)
|
|
|
|
|
|
|
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
|
|
|
|
an_instance_of(GRPC::DeadlineExceeded), project_id: project.id
|
|
|
|
)
|
|
|
|
expect { pipeline_status.load_status }.not_to raise_error
|
|
|
|
end
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
|
|
|
|
2017-07-11 03:35:47 +00:00
|
|
|
describe "#load_from_project", :clean_gitlab_redis_cache do
|
2017-03-14 09:05:45 +00:00
|
|
|
let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
|
|
|
|
|
|
|
|
it 'reads the status from the pipeline for the commit' do
|
2017-04-06 12:20:29 +00:00
|
|
|
pipeline_status.load_from_project
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
expect(pipeline_status.status).to eq('success')
|
|
|
|
expect(pipeline_status.sha).to eq(project.commit.sha)
|
2017-04-06 12:20:29 +00:00
|
|
|
expect(pipeline_status.ref).to eq(project.default_branch)
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
2017-03-14 19:46:34 +00:00
|
|
|
|
|
|
|
it "doesn't fail for an empty project" do
|
2017-08-02 19:55:11 +00:00
|
|
|
status_for_empty_commit = described_class.new(create(:project))
|
2017-03-14 19:46:34 +00:00
|
|
|
|
|
|
|
status_for_empty_commit.load_status
|
|
|
|
|
2018-11-23 08:41:08 +00:00
|
|
|
expect(status_for_empty_commit.sha).to be_nil
|
|
|
|
expect(status_for_empty_commit.status).to be_nil
|
|
|
|
expect(status_for_empty_commit.ref).to be_nil
|
2017-03-14 19:46:34 +00:00
|
|
|
end
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
|
|
|
|
2017-07-11 03:35:47 +00:00
|
|
|
describe "#store_in_cache", :clean_gitlab_redis_cache do
|
|
|
|
it "sets the object in caching" do
|
2017-03-14 09:05:45 +00:00
|
|
|
pipeline_status.sha = '123456'
|
|
|
|
pipeline_status.status = 'failed'
|
|
|
|
|
|
|
|
pipeline_status.store_in_cache
|
2017-07-11 03:35:47 +00:00
|
|
|
read_sha, read_status = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status) }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
expect(read_sha).to eq('123456')
|
|
|
|
expect(read_status).to eq('failed')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-07-11 03:35:47 +00:00
|
|
|
describe '#store_in_cache_if_needed', :clean_gitlab_redis_cache do
|
2017-03-14 09:05:45 +00:00
|
|
|
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)
|
2017-04-26 12:04:22 +00:00
|
|
|
pipeline_status = described_class.load_for_project(project)
|
2017-03-14 09:05:45 +00:00
|
|
|
|
2017-04-26 12:04:22 +00:00
|
|
|
pipeline_status.store_in_cache_if_needed
|
2017-07-11 03:35:47 +00:00
|
|
|
sha, status, ref = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status, :ref) }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
expect(sha).not_to be_nil
|
|
|
|
expect(status).not_to be_nil
|
2017-04-06 12:20:29 +00:00
|
|
|
expect(ref).not_to be_nil
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
|
|
|
|
2017-07-11 03:35:47 +00:00
|
|
|
it "doesn't store the status in redis_cache when the sha is not the head of the project" do
|
2017-04-26 12:04:22 +00:00
|
|
|
other_status = described_class.new(
|
|
|
|
project,
|
|
|
|
pipeline_info: { sha: "123456", status: "failed" }
|
|
|
|
)
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
other_status.store_in_cache_if_needed
|
2017-07-11 03:35:47 +00:00
|
|
|
sha, status = Gitlab::Redis::Cache.with { |redis| redis.hmget(cache_key, :sha, :status) }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
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
|
2017-08-02 19:55:11 +00:00
|
|
|
empty_project = create(:project)
|
2017-07-11 03:35:47 +00:00
|
|
|
Gitlab::Redis::Cache.with do |redis|
|
2017-04-26 12:04:22 +00:00
|
|
|
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"
|
|
|
|
})
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
other_status.store_in_cache_if_needed
|
2017-07-11 03:35:47 +00:00
|
|
|
sha, status, ref = Gitlab::Redis::Cache.with { |redis| redis.hmget("projects/#{empty_project.id}/pipeline_status", :sha, :status, :ref) }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
expect(sha).to be_nil
|
|
|
|
expect(status).to be_nil
|
2017-04-06 12:20:29 +00:00
|
|
|
expect(ref).to be_nil
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-07-11 03:35:47 +00:00
|
|
|
describe "with a status in caching", :clean_gitlab_redis_cache do
|
2017-03-14 09:05:45 +00:00
|
|
|
let(:status) { 'success' }
|
|
|
|
let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
|
2017-04-26 12:04:22 +00:00
|
|
|
let(:ref) { 'master' }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
before do
|
2017-07-11 03:35:47 +00:00
|
|
|
Gitlab::Redis::Cache.with do |redis|
|
2017-04-26 12:04:22 +00:00
|
|
|
redis.mapped_hmset(cache_key,
|
|
|
|
{ sha: sha, status: status, ref: ref })
|
|
|
|
end
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
describe '#load_from_cache' do
|
2017-07-11 03:35:47 +00:00
|
|
|
it 'reads the status from redis_cache' do
|
2017-03-14 09:05:45 +00:00
|
|
|
pipeline_status.load_from_cache
|
|
|
|
|
|
|
|
expect(pipeline_status.sha).to eq(sha)
|
|
|
|
expect(pipeline_status.status).to eq(status)
|
2017-04-26 12:04:22 +00:00
|
|
|
expect(pipeline_status.ref).to eq(ref)
|
2017-03-14 09:05:45 +00:00
|
|
|
end
|
2018-10-26 18:06:34 +00:00
|
|
|
|
|
|
|
context 'when status is empty string' do
|
|
|
|
before do
|
|
|
|
Gitlab::Redis::Cache.with do |redis|
|
|
|
|
redis.mapped_hmset(cache_key,
|
|
|
|
{ sha: sha, status: '', ref: ref })
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'reads the status as nil' do
|
|
|
|
pipeline_status.load_from_cache
|
|
|
|
|
|
|
|
expect(pipeline_status.status).to eq(nil)
|
|
|
|
end
|
|
|
|
end
|
2017-03-14 09:05:45 +00:00
|
|
|
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
|
2019-01-16 12:09:29 +00:00
|
|
|
it 'deletes values from redis_cache' do
|
2017-03-14 09:05:45 +00:00
|
|
|
pipeline_status.delete_from_cache
|
|
|
|
|
2017-07-11 03:35:47 +00:00
|
|
|
key_exists = Gitlab::Redis::Cache.with { |redis| redis.exists(cache_key) }
|
2017-03-14 09:05:45 +00:00
|
|
|
|
|
|
|
expect(key_exists).to be_falsy
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|