rails/activerecord/test/cases/test_case.rb
Brad Price 0a5b41c4c0 Check that entire collection has been loaded before short circuiting
Currently, when checking that the collection has been loaded, only the first
record is checked. In specific scenarios, if a record is fetched via an `after_initialize`
hook, there is a chance that the first record has been loaded, but other records in the
collection have not.

In order to successfully short circuit the fetching of data, we need to verify that
all of the records in the collection have been loaded.

* Create test for edge case
* Move `reset_callbacks` method to `cases/helper`, since it has been defined in multiple
  locations.

Closes #37730
2019-11-19 15:32:36 -06:00

143 lines
4.2 KiB
Ruby

# frozen_string_literal: true
require "active_support"
require "active_support/testing/autorun"
require "active_support/testing/method_call_assertions"
require "active_support/testing/stream"
require "active_record/fixtures"
require "cases/validations_repair_helper"
module ActiveRecord
# = Active Record Test Case
#
# Defines some test assertions to test against SQL queries.
class TestCase < ActiveSupport::TestCase #:nodoc:
include ActiveSupport::Testing::MethodCallAssertions
include ActiveSupport::Testing::Stream
include ActiveRecord::TestFixtures
include ActiveRecord::ValidationsRepairHelper
self.fixture_path = FIXTURES_ROOT
self.use_instantiated_fixtures = false
self.use_transactional_tests = true
def create_fixtures(*fixture_set_names, &block)
ActiveRecord::FixtureSet.create_fixtures(ActiveRecord::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block)
end
def teardown
SQLCounter.clear_log
end
def capture_sql
ActiveRecord::Base.connection.materialize_transactions
SQLCounter.clear_log
yield
SQLCounter.log.dup
end
def assert_sql(*patterns_to_match)
capture_sql { yield }
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
failed_patterns << pattern unless SQLCounter.log_all.any? { |sql| pattern === sql }
end
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
end
def assert_queries(num = 1, options = {})
ignore_none = options.fetch(:ignore_none) { num == :any }
ActiveRecord::Base.connection.materialize_transactions
SQLCounter.clear_log
x = yield
the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
if num == :any
assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
else
mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
assert_equal num, the_log.size, mesg
end
x
end
def assert_no_queries(options = {}, &block)
options.reverse_merge! ignore_none: true
assert_queries(0, options, &block)
end
def assert_column(model, column_name, msg = nil)
assert has_column?(model, column_name), msg
end
def assert_no_column(model, column_name, msg = nil)
assert_not has_column?(model, column_name), msg
end
def has_column?(model, column_name)
model.reset_column_information
model.column_names.include?(column_name.to_s)
end
def with_has_many_inversing
old = ActiveRecord::Base.has_many_inversing
ActiveRecord::Base.has_many_inversing = true
yield
ensure
ActiveRecord::Base.has_many_inversing = old
end
def reset_callbacks(klass, kind)
old_callbacks = {}
old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup
klass.subclasses.each do |subclass|
old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup
end
yield
ensure
klass.send("_#{kind}_callbacks=", old_callbacks[klass])
klass.subclasses.each do |subclass|
subclass.send("_#{kind}_callbacks=", old_callbacks[subclass])
end
end
end
class PostgreSQLTestCase < TestCase
def self.run(*args)
super if current_adapter?(:PostgreSQLAdapter)
end
end
class Mysql2TestCase < TestCase
def self.run(*args)
super if current_adapter?(:Mysql2Adapter)
end
end
class SQLite3TestCase < TestCase
def self.run(*args)
super if current_adapter?(:SQLite3Adapter)
end
end
class SQLCounter
class << self
attr_accessor :ignored_sql, :log, :log_all
def clear_log; self.log = []; self.log_all = []; end
end
clear_log
def call(name, start, finish, message_id, values)
return if values[:cached]
sql = values[:sql]
self.class.log_all << sql
self.class.log << sql unless ["SCHEMA", "TRANSACTION"].include? values[:name]
end
end
ActiveSupport::Notifications.subscribe("sql.active_record", SQLCounter.new)
end