Refactor relation preloading (#37217)
This commit is contained in:
parent
4af8e83c8a
commit
1766616ebc
@ -41,8 +41,8 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
|
|||||||
if current_account&.id != @status.account_id
|
if current_account&.id != @status.account_id
|
||||||
domains = @statuses.filter_map(&:account_domain).uniq
|
domains = @statuses.filter_map(&:account_domain).uniq
|
||||||
account_ids = @statuses.map(&:account_id).uniq
|
account_ids = @statuses.map(&:account_id).uniq
|
||||||
relations = current_account&.relations_map(account_ids, domains) || {}
|
current_account&.preload_relations!(account_ids, domains)
|
||||||
@statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? }
|
@statuses.reject! { |status| StatusFilter.new(status, current_account).filtered? }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,9 @@
|
|||||||
class StatusFilter
|
class StatusFilter
|
||||||
attr_reader :status, :account
|
attr_reader :status, :account
|
||||||
|
|
||||||
def initialize(status, account, preloaded_relations = {})
|
def initialize(status, account)
|
||||||
@status = status
|
@status = status
|
||||||
@account = account
|
@account = account
|
||||||
@preloaded_relations = preloaded_relations
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered?
|
def filtered?
|
||||||
@ -40,15 +39,15 @@ class StatusFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def blocking_account?
|
def blocking_account?
|
||||||
@preloaded_relations[:blocking] ? @preloaded_relations[:blocking][status.account_id] : account.blocking?(status.account_id)
|
account.blocking?(status.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocking_domain?
|
def blocking_domain?
|
||||||
@preloaded_relations[:domain_blocking_by_domain] ? @preloaded_relations[:domain_blocking_by_domain][status.account_domain] : account.domain_blocking?(status.account_domain)
|
account.domain_blocking?(status.account_domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def muting_account?
|
def muting_account?
|
||||||
@preloaded_relations[:muting] ? @preloaded_relations[:muting][status.account_id] : account.muting?(status.account_id)
|
account.muting?(status.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def silenced_account?
|
def silenced_account?
|
||||||
@ -60,7 +59,7 @@ class StatusFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_following_status_account?
|
def account_following_status_account?
|
||||||
@preloaded_relations[:following] ? @preloaded_relations[:following][status.account_id] : account&.following?(status.account_id)
|
account&.following?(status.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def blocked_by_policy?
|
def blocked_by_policy?
|
||||||
@ -68,6 +67,6 @@ class StatusFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def policy_allows_show?
|
def policy_allows_show?
|
||||||
StatusPolicy.new(account, status, @preloaded_relations).show?
|
StatusPolicy.new(account, status).show?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -123,7 +123,11 @@ module Account::Interactions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def following?(other_account)
|
def following?(other_account)
|
||||||
active_relationships.exists?(target_account: other_account)
|
other_id = other_account.is_a?(Account) ? other_account.id : other_account
|
||||||
|
|
||||||
|
preloaded_relation(:following, other_id) do
|
||||||
|
active_relationships.exists?(target_account: other_account)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def following_anyone?
|
def following_anyone?
|
||||||
@ -139,15 +143,33 @@ module Account::Interactions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def blocking?(other_account)
|
def blocking?(other_account)
|
||||||
block_relationships.exists?(target_account: other_account)
|
other_id = other_account.is_a?(Account) ? other_account.id : other_account
|
||||||
|
|
||||||
|
preloaded_relation(:blocking, other_id) do
|
||||||
|
block_relationships.exists?(target_account: other_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocked_by?(other_account)
|
||||||
|
other_id = other_account.is_a?(Account) ? other_account.id : other_account
|
||||||
|
|
||||||
|
preloaded_relation(:blocked_by, other_id) do
|
||||||
|
other_account.block_relationships.exists?(target_account: self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_blocking?(other_domain)
|
def domain_blocking?(other_domain)
|
||||||
domain_blocks.exists?(domain: other_domain)
|
preloaded_relation(:domain_blocking_by_domain, other_domain) do
|
||||||
|
domain_blocks.exists?(domain: other_domain)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def muting?(other_account)
|
def muting?(other_account)
|
||||||
mute_relationships.exists?(target_account: other_account)
|
other_id = other_account.is_a?(Account) ? other_account.id : other_account
|
||||||
|
|
||||||
|
preloaded_relation(:muting, other_id) do
|
||||||
|
mute_relationships.exists?(target_account: other_account)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def muting_conversation?(conversation)
|
def muting_conversation?(conversation)
|
||||||
@ -226,4 +248,10 @@ module Account::Interactions
|
|||||||
def normalized_domain(domain)
|
def normalized_domain(domain)
|
||||||
TagManager.instance.normalize_domain(domain)
|
TagManager.instance.normalize_domain(domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def preloaded_relation(type, key)
|
||||||
|
@preloaded_relations && @preloaded_relations[type] ? @preloaded_relations[type][key].present? : yield
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -91,6 +91,12 @@ module Account::Mappings
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def preload_relations!(...)
|
||||||
|
@preloaded_relations = relations_map(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def relations_map(account_ids, domains = nil, **options)
|
def relations_map(account_ids, domains = nil, **options)
|
||||||
relations = {
|
relations = {
|
||||||
blocked_by: Account.blocked_by_map(account_ids, id),
|
blocked_by: Account.blocked_by_map(account_ids, id),
|
||||||
|
|||||||
@ -26,7 +26,7 @@ module Status::InteractionPolicyConcern
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Returns `:automatic`, `:manual`, `:unknown` or `:denied`
|
# Returns `:automatic`, `:manual`, `:unknown` or `:denied`
|
||||||
def quote_policy_for_account(other_account, preloaded_relations: {})
|
def quote_policy_for_account(other_account)
|
||||||
return :denied if other_account.nil? || direct_visibility? || reblog?
|
return :denied if other_account.nil? || direct_visibility? || reblog?
|
||||||
|
|
||||||
following_author = nil
|
following_author = nil
|
||||||
@ -41,7 +41,7 @@ module Status::InteractionPolicyConcern
|
|||||||
return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||||
|
|
||||||
if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||||
following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil?
|
following_author = other_account.following?(account) if following_author.nil?
|
||||||
return :automatic if following_author
|
return :automatic if following_author
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ module Status::InteractionPolicyConcern
|
|||||||
return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||||
|
|
||||||
if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||||
following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil?
|
following_author = other_account.following?(account) if following_author.nil?
|
||||||
return :manual if following_author
|
return :manual if following_author
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,10 @@ module Status::ThreadingConcern
|
|||||||
statuses = Status.with_accounts(ids).to_a
|
statuses = Status.with_accounts(ids).to_a
|
||||||
account_ids = statuses.map(&:account_id).uniq
|
account_ids = statuses.map(&:account_id).uniq
|
||||||
domains = statuses.filter_map(&:account_domain).uniq
|
domains = statuses.filter_map(&:account_domain).uniq
|
||||||
relations = account&.relations_map(account_ids, domains) || {}
|
|
||||||
|
|
||||||
statuses.reject! { |status| StatusFilter.new(status, account, relations).filtered? }
|
account&.preload_relations!(account_ids, domains)
|
||||||
|
|
||||||
|
statuses.reject! { |status| StatusFilter.new(status, account).filtered? }
|
||||||
|
|
||||||
if stable
|
if stable
|
||||||
statuses.sort_by! { |status| ids.index(status.id) }
|
statuses.sort_by! { |status| ids.index(status.id) }
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::StatusPolicy < ApplicationPolicy
|
class Admin::StatusPolicy < ApplicationPolicy
|
||||||
def initialize(current_account, record, preloaded_relations = {})
|
|
||||||
super(current_account, record)
|
|
||||||
|
|
||||||
@preloaded_relations = preloaded_relations
|
|
||||||
end
|
|
||||||
|
|
||||||
def index?
|
def index?
|
||||||
role.can?(:manage_reports, :manage_users)
|
role.can?(:manage_reports, :manage_users)
|
||||||
end
|
end
|
||||||
@ -34,6 +28,6 @@ class Admin::StatusPolicy < ApplicationPolicy
|
|||||||
end
|
end
|
||||||
|
|
||||||
def viewable_through_normal_policy?
|
def viewable_through_normal_policy?
|
||||||
StatusPolicy.new(current_account, record, @preloaded_relations).show?
|
StatusPolicy.new(current_account, record).show?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class StatusPolicy < ApplicationPolicy
|
class StatusPolicy < ApplicationPolicy
|
||||||
def initialize(current_account, record, preloaded_relations = {})
|
|
||||||
super(current_account, record)
|
|
||||||
|
|
||||||
@preloaded_relations = preloaded_relations
|
|
||||||
end
|
|
||||||
|
|
||||||
def show?
|
def show?
|
||||||
return false if author.unavailable?
|
return false if author.unavailable?
|
||||||
|
|
||||||
@ -21,7 +15,7 @@ class StatusPolicy < ApplicationPolicy
|
|||||||
|
|
||||||
# This is about requesting a quote post, not validating it
|
# This is about requesting a quote post, not validating it
|
||||||
def quote?
|
def quote?
|
||||||
show? && record.quote_policy_for_account(current_account, preloaded_relations: @preloaded_relations) != :denied
|
show? && record.quote_policy_for_account(current_account) != :denied
|
||||||
end
|
end
|
||||||
|
|
||||||
def reblog?
|
def reblog?
|
||||||
@ -75,19 +69,19 @@ class StatusPolicy < ApplicationPolicy
|
|||||||
def blocking_author?
|
def blocking_author?
|
||||||
return false if current_account.nil?
|
return false if current_account.nil?
|
||||||
|
|
||||||
@preloaded_relations[:blocking] ? @preloaded_relations[:blocking][author.id] : current_account.blocking?(author)
|
current_account.blocking?(author)
|
||||||
end
|
end
|
||||||
|
|
||||||
def author_blocking?
|
def author_blocking?
|
||||||
return false if current_account.nil?
|
return false if current_account.nil?
|
||||||
|
|
||||||
@preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account)
|
current_account.blocked_by?(author)
|
||||||
end
|
end
|
||||||
|
|
||||||
def following_author?
|
def following_author?
|
||||||
return false if current_account.nil?
|
return false if current_account.nil?
|
||||||
|
|
||||||
@preloaded_relations[:following] ? @preloaded_relations[:following][author.id] : current_account.following?(author)
|
current_account.following?(author)
|
||||||
end
|
end
|
||||||
|
|
||||||
def author
|
def author
|
||||||
|
|||||||
@ -41,14 +41,8 @@ class StatusRelationshipsPresenter
|
|||||||
end
|
end
|
||||||
|
|
||||||
# This one is currently on-demand as it is only used for quote posts
|
# This one is currently on-demand as it is only used for quote posts
|
||||||
def preloaded_account_relations
|
def authoring_accounts
|
||||||
@preloaded_account_relations ||= begin
|
@authoring_accounts ||= @statuses.compact.flat_map { |s| [s.account, s.proper.account, s.proper.quote&.quoted_account] }.uniq.compact
|
||||||
accounts = @statuses.compact.flat_map { |s| [s.account, s.proper.account, s.proper.quote&.quoted_account] }.uniq.compact
|
|
||||||
|
|
||||||
account_ids = accounts.pluck(:id)
|
|
||||||
account_domains = accounts.pluck(:domain).uniq
|
|
||||||
Account.find(@current_account_id).relations_map(account_ids, account_domains)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@ -19,6 +19,13 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer
|
|||||||
private
|
private
|
||||||
|
|
||||||
def status_filter
|
def status_filter
|
||||||
@status_filter ||= StatusFilter.new(object.quoted_status, current_user&.account, instance_options[:relationships]&.preloaded_account_relations || {})
|
@status_filter ||= begin
|
||||||
|
if current_user && instance_options[:relationships]
|
||||||
|
account_ids = instance_options[:relationships].authoring_accounts.pluck(:id)
|
||||||
|
domains = instance_options[:relationships].authoring_accounts.pluck(:domain).uniq
|
||||||
|
current_user.account.preload_relations!(account_ids, domains)
|
||||||
|
end
|
||||||
|
StatusFilter.new(object.quoted_status, current_user&.account)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -29,9 +29,10 @@ class StatusesSearchService < BaseService
|
|||||||
results = request.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
|
results = request.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
|
||||||
account_ids = results.map(&:account_id)
|
account_ids = results.map(&:account_id)
|
||||||
account_domains = results.map(&:account_domain)
|
account_domains = results.map(&:account_domain)
|
||||||
preloaded_relations = @account.relations_map(account_ids, account_domains)
|
|
||||||
|
|
||||||
results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
|
@account.preload_relations!(account_ids, account_domains)
|
||||||
|
|
||||||
|
results.reject { |status| StatusFilter.new(status, @account).filtered? }
|
||||||
rescue Faraday::ConnectionFailed, Parslet::ParseFailed, Errno::ENETUNREACH
|
rescue Faraday::ConnectionFailed, Parslet::ParseFailed, Errno::ENETUNREACH
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|||||||
@ -302,9 +302,24 @@ RSpec.describe Account::Interactions do
|
|||||||
subject { account.following?(target_account) }
|
subject { account.following?(target_account) }
|
||||||
|
|
||||||
context 'when following target_account' do
|
context 'when following target_account' do
|
||||||
it 'returns true' do
|
before do
|
||||||
account.active_relationships.create(target_account: target_account)
|
account.active_relationships.create(target_account: target_account)
|
||||||
expect(subject).to be true
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to execute_queries
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when relations are preloaded' do
|
||||||
|
it 'does not query the database to get the result' do
|
||||||
|
account.preload_relations!([target_account.id])
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to_not execute_queries
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -336,9 +351,26 @@ RSpec.describe Account::Interactions do
|
|||||||
subject { account.blocking?(target_account) }
|
subject { account.blocking?(target_account) }
|
||||||
|
|
||||||
context 'when blocking target_account' do
|
context 'when blocking target_account' do
|
||||||
it 'returns true' do
|
before do
|
||||||
account.block_relationships.create(target_account: target_account)
|
account.block_relationships.create(target_account: target_account)
|
||||||
expect(subject).to be true
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to execute_queries
|
||||||
|
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when relations are preloaded' do
|
||||||
|
it 'does not query the database to get the result' do
|
||||||
|
account.preload_relations!([target_account.id])
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to_not execute_queries
|
||||||
|
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -349,16 +381,65 @@ RSpec.describe Account::Interactions do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#blocked_by?' do
|
||||||
|
subject { account.blocked_by?(target_account) }
|
||||||
|
|
||||||
|
context 'when blocked by target_account' do
|
||||||
|
before do
|
||||||
|
target_account.block_relationships.create(target_account: account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to execute_queries
|
||||||
|
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when relations are preloaded' do
|
||||||
|
it 'does not query the database to get the result' do
|
||||||
|
account.preload_relations!([target_account.id])
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to_not execute_queries
|
||||||
|
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not blocked by target_account' do
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#domain_blocking?' do
|
describe '#domain_blocking?' do
|
||||||
subject { account.domain_blocking?(domain) }
|
subject { account.domain_blocking?(domain) }
|
||||||
|
|
||||||
let(:domain) { 'example.com' }
|
let(:domain) { 'example.com' }
|
||||||
|
|
||||||
context 'when blocking the domain' do
|
context 'when blocking the domain' do
|
||||||
it 'returns true' do
|
before do
|
||||||
account_domain_block = Fabricate(:account_domain_block, domain: domain)
|
account_domain_block = Fabricate(:account_domain_block, domain: domain)
|
||||||
account.domain_blocks << account_domain_block
|
account.domain_blocks << account_domain_block
|
||||||
expect(subject).to be true
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to execute_queries
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when relations are preloaded' do
|
||||||
|
it 'does not query the database to get the result' do
|
||||||
|
account.preload_relations!([], [domain])
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to_not execute_queries
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -373,10 +454,25 @@ RSpec.describe Account::Interactions do
|
|||||||
subject { account.muting?(target_account) }
|
subject { account.muting?(target_account) }
|
||||||
|
|
||||||
context 'when muting target_account' do
|
context 'when muting target_account' do
|
||||||
it 'returns true' do
|
before do
|
||||||
mute = Fabricate(:mute, account: account, target_account: target_account)
|
mute = Fabricate(:mute, account: account, target_account: target_account)
|
||||||
account.mute_relationships << mute
|
account.mute_relationships << mute
|
||||||
expect(subject).to be true
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to execute_queries
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when relations are preloaded' do
|
||||||
|
it 'does not query the database to get the result' do
|
||||||
|
account.preload_relations!([target_account.id])
|
||||||
|
|
||||||
|
result = nil
|
||||||
|
expect { result = subject }.to_not execute_queries
|
||||||
|
expect(result).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
25
spec/support/matchers/sql_queries.rb
Normal file
25
spec/support/matchers/sql_queries.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'active_record/testing/query_assertions'
|
||||||
|
|
||||||
|
# Implement something similar to Rails' built-in assertion.
|
||||||
|
# Can be removed once https://github.com/rspec/rspec-rails/pull/2818
|
||||||
|
# has been merged and released.
|
||||||
|
RSpec::Matchers.define :execute_queries do |expected = nil|
|
||||||
|
match do |actual|
|
||||||
|
counter = ActiveRecord::Assertions::QueryAssertions::SQLCounter.new
|
||||||
|
|
||||||
|
queries = ActiveSupport::Notifications.subscribed(counter, 'sql.active_record') do
|
||||||
|
actual.call
|
||||||
|
counter.log
|
||||||
|
end
|
||||||
|
|
||||||
|
if expected.nil?
|
||||||
|
queries.count >= 1
|
||||||
|
else
|
||||||
|
queries.count == expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
supports_block_expectations
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user