debian-mirror-gitlab/spec/support/matchers/exceed_query_limit.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

383 lines
8.5 KiB
Ruby
Raw Normal View History

2019-10-12 21:52:04 +05:30
# frozen_string_literal: true
2018-11-08 19:23:39 +05:30
module ExceedQueryLimitHelpers
2022-07-23 23:45:48 +05:30
class QueryDiff
def initialize(expected, actual, show_common_queries)
@expected = expected
@actual = actual
@show_common_queries = show_common_queries
end
def diff
return combined_counts if @show_common_queries
combined_counts
.transform_values { select_suffixes_with_diffs(_1) }
.reject { |_prefix, suffs| suffs.empty? }
end
private
def select_suffixes_with_diffs(suffs)
reject_groups_with_different_parameters(reject_suffixes_with_identical_counts(suffs))
end
def reject_suffixes_with_identical_counts(suffs)
suffs.reject { |_k, counts| counts.first == counts.second }
end
# Eliminates groups that differ only in parameters,
# to make it easier to debug the output.
#
# For example, if we have a group `SELECT * FROM users...`,
# with the following suffixes
# `WHERE id = 1` (counts: N, 0)
# `WHERE id = 2` (counts: 0, N)
def reject_groups_with_different_parameters(suffs)
return suffs if suffs.size != 2
counts_a, counts_b = suffs.values
return {} if counts_a == counts_b.reverse && counts_a.include?(0)
suffs
end
def expected_counts
@expected.transform_values do |suffixes|
suffixes.transform_values { |n| [n, 0] }
end
end
def recorded_counts
@actual.transform_values do |suffixes|
suffixes.transform_values { |n| [0, n] }
end
end
def combined_counts
expected_counts.merge(recorded_counts) do |_k, exp, got|
exp.merge(got) do |_k, exp_counts, got_counts|
exp_counts.zip(got_counts).map { |a, b| a + b }
end
end
end
end
2023-03-04 22:38:38 +05:30
MARGINALIA_ANNOTATION_REGEX = %r{\s*/\*.*\*/}.freeze
DB_QUERY_RE = Regexp.union(
[
/^(?<prefix>SELECT .* FROM "?[a-z_]+"?) (?<suffix>.*)$/m,
/^(?<prefix>UPDATE "?[a-z_]+"?) (?<suffix>.*)$/m,
/^(?<prefix>INSERT INTO "[a-z_]+" \((?:"[a-z_]+",?\s?)+\)) (?<suffix>.*)$/m,
/^(?<prefix>DELETE FROM "[a-z_]+") (?<suffix>.*)$/m
]
).freeze
2021-02-22 17:27:13 +05:30
2018-03-17 18:26:18 +05:30
def with_threshold(threshold)
@threshold = threshold
self
end
def for_query(query)
@query = query
self
end
2021-04-29 21:17:54 +05:30
def for_model(model)
table = model.table_name if model < ActiveRecord::Base
for_query(/(FROM|UPDATE|INSERT INTO|DELETE FROM)\s+"#{table}"/)
end
2021-02-22 17:27:13 +05:30
def show_common_queries
@show_common_queries = true
self
end
def ignoring(pattern)
@ignoring_pattern = pattern
self
end
2018-03-17 18:26:18 +05:30
def threshold
@threshold.to_i
end
def expected_count
if expected.is_a?(ActiveRecord::QueryRecorder)
2021-02-22 17:27:13 +05:30
query_recorder_count(expected)
2018-03-17 18:26:18 +05:30
else
expected
end
end
def actual_count
2021-02-22 17:27:13 +05:30
@actual_count ||= query_recorder_count(recorder)
end
def query_recorder_count(query_recorder)
return query_recorder.count unless @query || @ignoring_pattern
query_log(query_recorder).size
end
def query_log(query_recorder)
filtered = query_recorder.log
filtered = filtered.select { |q| q =~ @query } if @query
filtered = filtered.reject { |q| q =~ @ignoring_pattern } if @ignoring_pattern
filtered
2018-03-17 18:26:18 +05:30
end
def recorder
2018-11-08 19:23:39 +05:30
@recorder ||= ActiveRecord::QueryRecorder.new(skip_cached: skip_cached, &@subject_block)
2018-03-17 18:26:18 +05:30
end
2021-02-22 17:27:13 +05:30
# Take a query recorder and tabulate the frequencies of suffixes for each prefix.
#
# @return Hash[String, Hash[String, Int]]
#
# Example:
#
# r = ActiveRecord::QueryRecorder.new do
# SomeTable.create(x: 1, y: 2, z: 3)
# SomeOtherTable.where(id: 1).first
# SomeTable.create(x: 4, y: 5, z: 6)
# SomeOtherTable.all
# end
# count_queries(r)
# #=>
# {
# 'INSERT INTO "some_table" VALUES' => {
# '(1,2,3)' => 1,
# '(4,5,6)' => 1
# },
# 'SELECT * FROM "some_other_table"' => {
# 'WHERE id = 1 LIMIT 1' => 1,
# '' => 2
# }
# }
def count_queries(query_recorder)
strip_marginalia_annotations(query_log(query_recorder))
.map { |q| query_group_key(q) }
.group_by { |k| k[:prefix] }
.transform_values { |keys| frequencies(:suffix, keys) }
end
def frequencies(key, things)
things.group_by { |x| x[key] }.transform_values(&:size)
end
def query_group_key(query)
DB_QUERY_RE.match(query) || { prefix: query, suffix: '' }
end
def diff_query_counts(expected, actual)
2022-07-23 23:45:48 +05:30
QueryDiff.new(expected, actual, @show_common_queries).diff
2021-02-22 17:27:13 +05:30
end
def diff_query_group_message(query, suffixes)
suffix_messages = suffixes.map do |s, counts|
"-- (expected: #{counts.first}, got: #{counts.second})\n #{s}"
end
"#{query}...\n#{suffix_messages.join("\n")}"
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def log_message
if expected.is_a?(ActiveRecord::QueryRecorder)
2021-02-22 17:27:13 +05:30
diff_counts = diff_query_counts(count_queries(expected), count_queries(@recorder))
2022-07-23 23:45:48 +05:30
sections = diff_counts.filter_map { |q, suffixes| diff_query_group_message(q, suffixes) }
2018-03-17 18:26:18 +05:30
2021-02-22 17:27:13 +05:30
<<~MSG
Query Diff:
-----------
#{sections.join("\n\n")}
MSG
2018-03-17 18:26:18 +05:30
else
@recorder.log_message
end
2017-08-17 22:00:37 +05:30
end
2018-11-08 19:23:39 +05:30
def skip_cached
true
end
def verify_count(&block)
@subject_block = block
2020-06-23 00:09:42 +05:30
actual_count > maximum
end
def maximum
expected_count + threshold
2018-11-08 19:23:39 +05:30
end
def failure_message
2020-06-23 00:09:42 +05:30
threshold_message = threshold > 0 ? " (+#{threshold})" : ''
2018-11-08 19:23:39 +05:30
counts = "#{expected_count}#{threshold_message}"
"Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
end
2020-04-08 14:13:33 +05:30
def strip_marginalia_annotations(logs)
logs.map { |log| log.sub(MARGINALIA_ANNOTATION_REGEX, '') }
end
2018-11-08 19:23:39 +05:30
end
2020-06-23 00:09:42 +05:30
RSpec::Matchers.define :issue_fewer_queries_than do
supports_block_expectations
include ExceedQueryLimitHelpers
def control
block_arg
end
def control_recorder
@control_recorder ||= ActiveRecord::QueryRecorder.new(&control)
end
def expected_count
control_recorder.count
end
def verify_count(&block)
@subject_block = block
# These blocks need to be evaluated in an expected order, in case
# the events in expected affect the counts in actual
expected_count
actual_count
actual_count < expected_count
end
match do |block|
verify_count(&block)
end
def failure_message
<<~MSG
Expected to issue fewer than #{expected_count} queries, but got #{actual_count}
#{log_message}
MSG
end
failure_message_when_negated do |actual|
<<~MSG
Expected query count of #{actual_count} to be less than #{expected_count}
#{log_message}
MSG
end
end
2020-04-22 19:07:51 +05:30
RSpec::Matchers.define :issue_same_number_of_queries_as do
supports_block_expectations
include ExceedQueryLimitHelpers
def control
block_arg
end
2020-06-23 00:09:42 +05:30
chain :or_fewer do
@or_fewer = true
end
chain :ignoring_cached_queries do
@skip_cached = true
end
2020-04-22 19:07:51 +05:30
def control_recorder
@control_recorder ||= ActiveRecord::QueryRecorder.new(&control)
end
def expected_count
2020-06-23 00:09:42 +05:30
control_recorder.count
2020-04-22 19:07:51 +05:30
end
def verify_count(&block)
@subject_block = block
2020-06-23 00:09:42 +05:30
# These blocks need to be evaluated in an expected order, in case
# the events in expected affect the counts in actual
expected_count
actual_count
if @or_fewer
actual_count <= expected_count
else
(expected_count - actual_count).abs <= threshold
end
2020-04-22 19:07:51 +05:30
end
match do |block|
verify_count(&block)
end
2020-06-23 00:09:42 +05:30
def failure_message
<<~MSG
Expected #{expected_count_message} queries, but got #{actual_count}
#{log_message}
MSG
end
2020-04-22 19:07:51 +05:30
failure_message_when_negated do |actual|
2020-06-23 00:09:42 +05:30
<<~MSG
Expected #{actual_count} not to equal #{expected_count_message}
#{log_message}
MSG
end
def expected_count_message
or_fewer_msg = "or fewer" if @or_fewer
2020-10-24 23:57:45 +05:30
threshold_msg = "(+/- #{threshold})" unless threshold == 0
2020-06-23 00:09:42 +05:30
2023-01-13 00:05:48 +05:30
[expected_count.to_s, or_fewer_msg, threshold_msg].compact.join(' ')
2020-04-22 19:07:51 +05:30
end
def skip_cached
2020-06-23 00:09:42 +05:30
@skip_cached || false
2020-04-22 19:07:51 +05:30
end
end
2018-11-08 19:23:39 +05:30
RSpec::Matchers.define :exceed_all_query_limit do |expected|
supports_block_expectations
include ExceedQueryLimitHelpers
match do |block|
verify_count(&block)
end
failure_message_when_negated do |actual|
failure_message
end
def skip_cached
false
end
end
# Excludes cached methods from the query count
RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations
include ExceedQueryLimitHelpers
match do |block|
2022-07-23 23:45:48 +05:30
if block.is_a?(ActiveRecord::QueryRecorder)
@recorder = block
verify_count
else
verify_count(&block)
end
2018-11-08 19:23:39 +05:30
end
failure_message_when_negated do |actual|
failure_message
end
2017-08-17 22:00:37 +05:30
end