summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/graphql/types/snippets/blob_type_spec.rb2
-rw-r--r--spec/lib/gitlab/redis/boolean_spec.rb150
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb3
-rw-r--r--spec/lib/gitlab/repository_hash_cache_spec.rb184
-rw-r--r--spec/lib/quality/test_level_spec.rb4
-rw-r--r--spec/models/repository_spec.rb59
-rw-r--r--spec/presenters/snippet_blob_presenter_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb6
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb6
-rw-r--r--spec/services/users/destroy_service_spec.rb10
-rw-r--r--spec/support/helpers/query_recorder.rb49
-rw-r--r--spec/support_specs/helpers/active_record/query_recorder_spec.rb40
12 files changed, 474 insertions, 51 deletions
diff --git a/spec/graphql/types/snippets/blob_type_spec.rb b/spec/graphql/types/snippets/blob_type_spec.rb
index a263fada644..b6253e96d60 100644
--- a/spec/graphql/types/snippets/blob_type_spec.rb
+++ b/spec/graphql/types/snippets/blob_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe GitlabSchema.types['SnippetBlob'] do
it 'has the correct fields' do
- expected_fields = [:highlighted_data, :plain_highlighted_data,
+ expected_fields = [:rich_data, :plain_data,
:raw_path, :size, :binary, :name, :path,
:simple_viewer, :rich_viewer, :mode]
diff --git a/spec/lib/gitlab/redis/boolean_spec.rb b/spec/lib/gitlab/redis/boolean_spec.rb
new file mode 100644
index 00000000000..bfacf0c448b
--- /dev/null
+++ b/spec/lib/gitlab/redis/boolean_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe Gitlab::Redis::Boolean do
+ subject(:redis_boolean) { described_class.new(bool) }
+
+ let(:bool) { true }
+ let(:label_section) { "#{described_class::LABEL}#{described_class::DELIMITER}" }
+
+ describe "#to_s" do
+ subject { redis_boolean.to_s }
+
+ context "true" do
+ let(:bool) { true }
+
+ it { is_expected.to eq("#{label_section}#{described_class::TRUE_STR}") }
+ end
+
+ context "false" do
+ let(:bool) { false }
+
+ it { is_expected.to eq("#{label_section}#{described_class::FALSE_STR}") }
+ end
+ end
+
+ describe ".encode" do
+ subject { redis_boolean.class.encode(bool) }
+
+ context "true" do
+ let(:bool) { true }
+
+ it { is_expected.to eq("#{label_section}#{described_class::TRUE_STR}") }
+ end
+
+ context "false" do
+ let(:bool) { false }
+
+ it { is_expected.to eq("#{label_section}#{described_class::FALSE_STR}") }
+ end
+ end
+
+ describe ".decode" do
+ subject { redis_boolean.class.decode(str) }
+
+ context "valid encoded bool" do
+ let(:str) { "#{label_section}#{bool_str}" }
+
+ context "true" do
+ let(:bool_str) { described_class::TRUE_STR }
+
+ it { is_expected.to be(true) }
+ end
+
+ context "false" do
+ let(:bool_str) { described_class::FALSE_STR }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context "partially invalid bool" do
+ let(:str) { "#{label_section}whoops" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
+ end
+ end
+
+ context "invalid encoded bool" do
+ let(:str) { "whoops" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
+ end
+ end
+ end
+
+ describe ".true?" do
+ subject { redis_boolean.class.true?(str) }
+
+ context "valid encoded bool" do
+ let(:str) { "#{label_section}#{bool_str}" }
+
+ context "true" do
+ let(:bool_str) { described_class::TRUE_STR }
+
+ it { is_expected.to be(true) }
+ end
+
+ context "false" do
+ let(:bool_str) { described_class::FALSE_STR }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
+ context "partially invalid bool" do
+ let(:str) { "#{label_section}whoops" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
+ end
+ end
+
+ context "invalid encoded bool" do
+ let(:str) { "whoops" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
+ end
+ end
+ end
+
+ describe ".false?" do
+ subject { redis_boolean.class.false?(str) }
+
+ context "valid encoded bool" do
+ let(:str) { "#{label_section}#{bool_str}" }
+
+ context "true" do
+ let(:bool_str) { described_class::TRUE_STR }
+
+ it { is_expected.to be(false) }
+ end
+
+ context "false" do
+ let(:bool_str) { described_class::FALSE_STR }
+
+ it { is_expected.to be(true) }
+ end
+ end
+
+ context "partially invalid bool" do
+ let(:str) { "#{label_section}whoops" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
+ end
+ end
+
+ context "invalid encoded bool" do
+ let(:str) { "whoops" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(described_class::NotAnEncodedBooleanStringError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index fd1338b55a6..b4fc504ea60 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -7,6 +7,7 @@ describe Gitlab::RepositoryCacheAdapter do
let(:repository) { project.repository }
let(:cache) { repository.send(:cache) }
let(:redis_set_cache) { repository.send(:redis_set_cache) }
+ let(:redis_hash_cache) { repository.send(:redis_hash_cache) }
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
@@ -212,6 +213,8 @@ describe Gitlab::RepositoryCacheAdapter do
expect(cache).to receive(:expire).with(:branch_names)
expect(redis_set_cache).to receive(:expire).with(:rendered_readme)
expect(redis_set_cache).to receive(:expire).with(:branch_names)
+ expect(redis_hash_cache).to receive(:delete).with(:rendered_readme)
+ expect(redis_hash_cache).to receive(:delete).with(:branch_names)
repository.expire_method_caches(%i(rendered_readme branch_names))
end
diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb
new file mode 100644
index 00000000000..014a2f235b9
--- /dev/null
+++ b/spec/lib/gitlab/repository_hash_cache_spec.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_cache do
+ let_it_be(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:namespace) { "#{repository.full_path}:#{project.id}" }
+ let(:cache) { described_class.new(repository) }
+ let(:test_hash) do
+ { "test" => "value" }
+ end
+
+ describe "#cache_key" do
+ subject { cache.cache_key(:example) }
+
+ it "includes the namespace" do
+ is_expected.to eq("example:#{namespace}:hash")
+ end
+
+ context "with a given namespace" do
+ let(:extra_namespace) { "my:data" }
+ let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
+
+ it "includes the full namespace" do
+ is_expected.to eq("example:#{namespace}:#{extra_namespace}:hash")
+ end
+ end
+ end
+
+ describe "#delete" do
+ subject { cache.delete(:example) }
+
+ context "key exists" do
+ before do
+ cache.write(:example, test_hash)
+ end
+
+ it { is_expected.to eq(1) }
+
+ it "deletes the given key from the cache" do
+ subject
+
+ expect(cache.read_members(:example, ["test"])).to eq({ "test" => nil })
+ end
+ end
+
+ context "key doesn't exist" do
+ it { is_expected.to eq(0) }
+ end
+ end
+
+ describe "#key?" do
+ subject { cache.key?(:example, "test") }
+
+ context "key exists" do
+ before do
+ cache.write(:example, test_hash)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context "key doesn't exist" do
+ it { is_expected.to be(false) }
+ end
+ end
+
+ describe "#read_members" do
+ subject { cache.read_members(:example, keys) }
+
+ let(:keys) { %w(test missing) }
+
+ context "all data is cached" do
+ before do
+ cache.write(:example, test_hash.merge({ "missing" => false }))
+ end
+
+ it { is_expected.to eq({ "test" => "value", "missing" => "false" }) }
+ end
+
+ context "partial data is cached" do
+ before do
+ cache.write(:example, test_hash)
+ end
+
+ it { is_expected.to eq({ "test" => "value", "missing" => nil }) }
+ end
+
+ context "no data is cached" do
+ it { is_expected.to eq({ "test" => nil, "missing" => nil }) }
+ end
+
+ context "empty keys are passed for some reason" do
+ let(:keys) { [] }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Gitlab::RepositoryHashCache::InvalidKeysProvidedError)
+ end
+ end
+ end
+
+ describe "#write" do
+ subject { cache.write(:example, test_hash) }
+
+ it { is_expected.to be(true) }
+
+ it "actually writes stuff to Redis" do
+ subject
+
+ expect(cache.read_members(:example, ["test"])).to eq(test_hash)
+ end
+ end
+
+ describe "#fetch_and_add_missing" do
+ subject do
+ cache.fetch_and_add_missing(:example, keys) do |missing_keys, hash|
+ missing_keys.each do |key|
+ hash[key] = "was_missing"
+ end
+ end
+ end
+
+ let(:keys) { %w(test) }
+
+ it "records metrics" do
+ # Here we expect it to receive "test" as a missing key because we
+ # don't write to the cache before this test
+ expect(cache).to receive(:record_metrics).with(:example, { "test" => "was_missing" }, ["test"])
+
+ subject
+ end
+
+ context "fully cached" do
+ let(:keys) { %w(test another) }
+
+ before do
+ cache.write(:example, test_hash.merge({ "another" => "not_missing" }))
+ end
+
+ it "returns a hash" do
+ is_expected.to eq({ "test" => "value", "another" => "not_missing" })
+ end
+
+ it "doesn't write to the cache" do
+ expect(cache).not_to receive(:write)
+
+ subject
+ end
+ end
+
+ context "partially cached" do
+ let(:keys) { %w(test missing) }
+
+ before do
+ cache.write(:example, test_hash)
+ end
+
+ it "returns a hash" do
+ is_expected.to eq({ "test" => "value", "missing" => "was_missing" })
+ end
+
+ it "writes to the cache" do
+ expect(cache).to receive(:write).with(:example, { "missing" => "was_missing" })
+
+ subject
+ end
+ end
+
+ context "uncached" do
+ let(:keys) { %w(test missing) }
+
+ it "returns a hash" do
+ is_expected.to eq({ "test" => "was_missing", "missing" => "was_missing" })
+ end
+
+ it "writes to the cache" do
+ expect(cache).to receive(:write).with(:example, { "test" => "was_missing", "missing" => "was_missing" })
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index 621a426a18d..757a003946b 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
end
end
@@ -82,7 +82,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|tasks|uploaders|validators|views|workers|elastic_integration)})
+ .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration)})
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 1e558131dc6..77114696fd2 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -500,45 +500,62 @@ describe Repository do
let(:branch_names) { %w(test beep boop definitely_merged) }
let(:already_merged) { Set.new(["definitely_merged"]) }
- let(:merge_state_hash) do
+ let(:write_hash) do
{
- "test" => false,
- "beep" => false,
- "boop" => false,
- "definitely_merged" => true
+ "test" => Gitlab::Redis::Boolean.new(false).to_s,
+ "beep" => Gitlab::Redis::Boolean.new(false).to_s,
+ "boop" => Gitlab::Redis::Boolean.new(false).to_s,
+ "definitely_merged" => Gitlab::Redis::Boolean.new(true).to_s
}
end
- let_it_be(:cache) do
- caching_config_hash = Gitlab::Redis::Cache.params
- ActiveSupport::Cache.lookup_store(:redis_cache_store, caching_config_hash)
- end
-
- let(:repository_cache) do
- Gitlab::RepositoryCache.new(repository, backend: Rails.cache)
+ let(:read_hash) do
+ {
+ "test" => Gitlab::Redis::Boolean.new(false).to_s,
+ "beep" => Gitlab::Redis::Boolean.new(false).to_s,
+ "boop" => Gitlab::Redis::Boolean.new(false).to_s,
+ "definitely_merged" => Gitlab::Redis::Boolean.new(true).to_s
+ }
end
- let(:cache_key) { repository_cache.cache_key(:merged_branch_names) }
+ let(:cache) { repository.send(:redis_hash_cache) }
+ let(:cache_key) { cache.cache_key(:merged_branch_names) }
before do
- allow(Rails).to receive(:cache) { cache }
- allow(repository).to receive(:cache) { repository_cache }
allow(repository.raw_repository).to receive(:merged_branch_names).with(branch_names).and_return(already_merged)
end
it { is_expected.to eq(already_merged) }
it { is_expected.to be_a(Set) }
+ describe "cache expiry" do
+ before do
+ allow(cache).to receive(:delete).with(anything)
+ end
+
+ it "is expired when the branches caches are expired" do
+ expect(cache).to receive(:delete).with(:merged_branch_names).at_least(:once)
+
+ repository.send(:expire_branches_cache)
+ end
+
+ it "is expired when the repository caches are expired" do
+ expect(cache).to receive(:delete).with(:merged_branch_names).at_least(:once)
+
+ repository.send(:expire_all_method_caches)
+ end
+ end
+
context "cache is empty" do
before do
- cache.delete(cache_key)
+ cache.delete(:merged_branch_names)
end
it { is_expected.to eq(already_merged) }
describe "cache values" do
it "writes the values to redis" do
- expect(cache).to receive(:write).with(cache_key, merge_state_hash, expires_in: Repository::MERGED_BRANCH_NAMES_CACHE_DURATION)
+ expect(cache).to receive(:write).with(:merged_branch_names, write_hash)
subject
end
@@ -546,14 +563,14 @@ describe Repository do
it "matches the supplied hash" do
subject
- expect(cache.read(cache_key)).to eq(merge_state_hash)
+ expect(cache.read_members(:merged_branch_names, branch_names)).to eq(read_hash)
end
end
end
context "cache is not empty" do
before do
- cache.write(cache_key, merge_state_hash)
+ cache.write(:merged_branch_names, write_hash)
end
it { is_expected.to eq(already_merged) }
@@ -568,8 +585,8 @@ describe Repository do
context "cache is partially complete" do
before do
allow(repository.raw_repository).to receive(:merged_branch_names).with(["boop"]).and_return([])
- hash = merge_state_hash.except("boop")
- cache.write(cache_key, hash)
+ hash = write_hash.except("boop")
+ cache.write(:merged_branch_names, hash)
end
it { is_expected.to eq(already_merged) }
diff --git a/spec/presenters/snippet_blob_presenter_spec.rb b/spec/presenters/snippet_blob_presenter_spec.rb
index 92893ec597a..fa10d1a7f30 100644
--- a/spec/presenters/snippet_blob_presenter_spec.rb
+++ b/spec/presenters/snippet_blob_presenter_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
describe SnippetBlobPresenter do
- describe '#highlighted_data' do
+ describe '#rich_data' do
let(:snippet) { build(:personal_snippet) }
- subject { described_class.new(snippet.blob).highlighted_data }
+ subject { described_class.new(snippet.blob).rich_data }
it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true)
@@ -18,7 +18,7 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test.md'
snippet.content = '*foo*'
- expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>'
+ expect(subject).to eq '<p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p>'
end
it 'returns syntax highlighted content' do
@@ -37,10 +37,10 @@ describe SnippetBlobPresenter do
end
end
- describe '#plain_highlighted_data' do
+ describe '#plain_data' do
let(:snippet) { build(:personal_snippet) }
- subject { described_class.new(snippet.blob).plain_highlighted_data }
+ subject { described_class.new(snippet.blob).plain_data }
it 'returns nil when the snippet blob is binary' do
allow(snippet.blob).to receive(:binary?).and_return(true)
@@ -52,7 +52,7 @@ describe SnippetBlobPresenter do
snippet.file_name = 'test.md'
snippet.content = '*foo*'
- expect(subject).to eq '<span id="LC1" class="line" lang="">*foo*</span>'
+ expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>'
end
it 'returns plain syntax content' do
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index 876eff8c753..cb19f50b5b5 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -67,7 +67,8 @@ describe 'Creating a Snippet' do
it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['snippet']['blob']['highlightedData']).to match(content)
+ expect(mutation_response['snippet']['blob']['richData']).to match(content)
+ expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name)
@@ -92,7 +93,8 @@ describe 'Creating a Snippet' do
it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['snippet']['blob']['highlightedData']).to match(content)
+ expect(mutation_response['snippet']['blob']['richData']).to match(content)
+ expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name)
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index f4c0b646c01..e9481a36287 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -56,7 +56,8 @@ describe 'Updating a Snippet' do
it 'returns the updated Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['snippet']['blob']['highlightedData']).to match(updated_content)
+ expect(mutation_response['snippet']['blob']['richData']).to match(updated_content)
+ expect(mutation_response['snippet']['blob']['plainData']).to match(updated_content)
expect(mutation_response['snippet']['title']).to eq(updated_title)
expect(mutation_response['snippet']['description']).to eq(updated_description)
expect(mutation_response['snippet']['fileName']).to eq(updated_file_name)
@@ -77,7 +78,8 @@ describe 'Updating a Snippet' do
it 'returns the Snippet with its original values' do
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['snippet']['blob']['highlightedData']).to match(original_content)
+ expect(mutation_response['snippet']['blob']['richData']).to match(original_content)
+ expect(mutation_response['snippet']['blob']['plainData']).to match(original_content)
expect(mutation_response['snippet']['title']).to eq(original_title)
expect(mutation_response['snippet']['description']).to eq(original_description)
expect(mutation_response['snippet']['fileName']).to eq(original_file_name)
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index f972fb4c5b9..2b658a93b0a 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -26,16 +26,6 @@ describe Users::DestroyService do
service.execute(user)
end
- context 'when :destroy_user_associations_in_batches flag is disabled' do
- it 'does not delete user associations in batches' do
- stub_feature_flags(destroy_user_associations_in_batches: false)
-
- expect(user).not_to receive(:destroy_dependent_associations_in_batches)
-
- service.execute(user)
- end
- end
-
it 'will delete the project' do
expect_next_instance_of(Projects::DestroyService) do |destroy_service|
expect(destroy_service).to receive(:execute).once.and_return(true)
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index 1d04014c9a6..fd200a1abf3 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -2,12 +2,15 @@
module ActiveRecord
class QueryRecorder
- attr_reader :log, :skip_cached, :cached
+ attr_reader :log, :skip_cached, :cached, :data
+ UNKNOWN = %w(unknown unknown).freeze
- def initialize(skip_cached: true, &block)
+ def initialize(skip_cached: true, query_recorder_debug: false, &block)
+ @data = Hash.new { |h, k| h[k] = { count: 0, occurrences: [], backtrace: [] } }
@log = []
@cached = []
@skip_cached = skip_cached
+ @query_recorder_debug = query_recorder_debug
# force replacement of bind parameters to give tests the ability to check for ids
ActiveRecord::Base.connection.unprepared_statement do
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
@@ -19,30 +22,62 @@ module ActiveRecord
Gitlab::BacktraceCleaner.clean_backtrace(caller).each { |line| Rails.logger.debug(" --> #{line}") }
end
+ def get_sql_source(sql)
+ matches = sql.match(/,line:(?<line>.*):in\s+`(?<method>.*)'\*\//)
+ matches ? [matches[:line], matches[:method]] : UNKNOWN
+ end
+
+ def store_sql_by_source(values: {}, backtrace: nil)
+ full_name = get_sql_source(values[:sql]).join(':')
+ @data[full_name][:count] += 1
+ @data[full_name][:occurrences] << values[:sql]
+ @data[full_name][:backtrace] << backtrace
+ end
+
+ def find_query(query_regexp, limit, first_only: false)
+ out = []
+
+ @data.each_pair do |k, v|
+ if v[:count] > limit && k.match(query_regexp)
+ out << [k, v[:count]]
+ break if first_only
+ end
+ end
+
+ out.flatten! if first_only
+ out
+ end
+
+ def occurrences_by_line_method
+ @occurrences_by_line_method ||= @data.sort_by { |_, v| v[:count] }
+ end
+
def callback(name, start, finish, message_id, values)
- show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
+ store_backtrace = ENV['QUERY_RECORDER_DEBUG'] || @query_recorder_debug
+ backtrace = store_backtrace ? show_backtrace(values) : nil
if values[:cached] && skip_cached
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql]
+ store_sql_by_source(values: values, backtrace: backtrace)
end
end
def count
- @log.count
+ @count ||= @log.count
end
def cached_count
- @cached.count
+ @cached_count ||= @cached.count
end
def log_message
- @log.join("\n\n")
+ @log_message ||= @log.join("\n\n")
end
def occurrences
- @log.group_by(&:to_s).transform_values(&:count)
+ @occurrences ||= @log.group_by(&:to_s).transform_values(&:count)
end
end
end
diff --git a/spec/support_specs/helpers/active_record/query_recorder_spec.rb b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
new file mode 100644
index 00000000000..48069c6a766
--- /dev/null
+++ b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ActiveRecord::QueryRecorder do
+ class TestQueries < ActiveRecord::Base
+ self.table_name = 'schema_migrations'
+ end
+
+ describe 'detecting the right number of calls and their origin' do
+ it 'detects two separate queries' do
+ control = ActiveRecord::QueryRecorder.new query_recorder_debug: true do
+ 2.times { TestQueries.count }
+ TestQueries.first
+ end
+
+ # Test first_only flag works as expected
+ expect(control.find_query(/.*query_recorder_spec.rb.*/, 0, first_only: true))
+ .to eq(control.find_query(/.*query_recorder_spec.rb.*/, 0).first)
+ # Check #find_query
+ expect(control.find_query(/.*/, 0).size)
+ .to eq(control.data.keys.size)
+ # Ensure exactly 2 COUNT queries were detected
+ expect(control.occurrences_by_line_method.last[1][:occurrences]
+ .find_all {|i| i.match(/SELECT COUNT/) }.count).to eq(2)
+ # Ensure exactly 1 LIMIT 1 (#first)
+ expect(control.occurrences_by_line_method.first[1][:occurrences]
+ .find_all { |i| i.match(/ORDER BY.*#{TestQueries.table_name}.*LIMIT 1/) }.count).to eq(1)
+
+ # Ensure 3 DB calls overall were executed
+ expect(control.log.size).to eq(3)
+ # Ensure memoization value match the raw value above
+ expect(control.count).to eq(control.log.size)
+ # Ensure we have only two sources of queries
+ expect(control.data.keys.size).to eq(2)
+ # Ensure we detect only queries from this file
+ expect(control.data.keys.find_all { |i| i.match(/query_recorder_spec.rb/) }.count).to eq(2)
+ end
+ end
+end