rails--rails/activerecord/test/cases/test_case.rb

158 lines
4.9 KiB
Ruby
Raw Normal View History

# 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.
2021-07-29 21:18:07 +00:00
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
Except SCHEMA SQLs in `capture_sql` Testing the result of `capture_sql` is fragile, it is due to whether SCHEMA SQLs are executed or not depends on whether schema cache is filled or not. https://buildkite.com/rails/rails/builds/61248#a5b9dc59-ff0c-40c0-b56e-0895662fbc4c/993-1004 https://buildkite.com/rails/rails/builds/61248#1157b389-f2c7-4554-b6e5-a37624a0e74a/996-1005 I've confirmed all `capture_sql` use cases in our code base, all cases won't expect SCHEMA SQLs are included. ``` % git grep -n capture_sql test/cases/associations/belongs_to_associations_test.rb:202: sql = capture_sql { comment.post } test/cases/associations/belongs_to_associations_test.rb:204: assert_not_equal sql, capture_sql { comment.post } test/cases/associations/has_many_associations_test.rb:169: sql = capture_sql { post.comments.to_a } test/cases/associations/has_many_associations_test.rb:171: assert_not_equal sql, capture_sql { post.comments.to_a } test/cases/associations/has_many_associations_test.rb:276: expected_sql = capture_sql { author.thinking_posts.delete_all } test/cases/associations/has_many_associations_test.rb:281: loaded_sql = capture_sql { author.thinking_posts.delete_all } test/cases/associations/has_many_associations_test.rb:289: expected_sql = capture_sql { author.posts.delete_all } test/cases/associations/has_many_associations_test.rb:294: loaded_sql = capture_sql { author.posts.delete_all } test/cases/associations/left_outer_join_association_test.rb:22: queries = capture_sql do test/cases/associations/left_outer_join_association_test.rb:49: queries = capture_sql { Author.left_outer_joins(:posts).to_a } test/cases/associations/left_outer_join_association_test.rb:54: queries = capture_sql { Author.joins(:posts).left_outer_joins(:posts).to_a } test/cases/associations/left_outer_join_association_test.rb:60: queries = capture_sql { Author.left_outer_joins({}).to_a } test/cases/associations/left_outer_join_association_test.rb:65: queries = capture_sql { Author.left_outer_joins([]).to_a } test/cases/associations/left_outer_join_association_test.rb:78: queries = capture_sql { Author.left_outer_joins(:essays).to_a } test/cases/associations_test.rb:384: log = capture_sql do test/cases/associations_test.rb:399: log = capture_sql do test/cases/associations_test.rb:414: log = capture_sql do test/cases/associations_test.rb:429: log = capture_sql do test/cases/associations_test.rb:444: log = capture_sql do test/cases/associations_test.rb:459: log = capture_sql do test/cases/reflection_test.rb:307: expected_sql = capture_sql { hotel.recipes.to_a } test/cases/reflection_test.rb:312: loaded_sql = capture_sql { hotel.recipes.to_a } test/cases/relation_test.rb:212: queries = capture_sql { Author.joins(:posts).merge(Post.joins(:comments)).to_a } test/cases/relation_test.rb:232: queries = capture_sql { Post.joins(:author, :categorizations).merge(Author.select(:id)).merge(categorizations_with_authors).to_a } test/cases/relation_test.rb:347: log = capture_sql do test/cases/scoping/relation_scoping_test.rb:146: log = capture_sql do test/cases/scoping/relation_scoping_test.rb:159: log = capture_sql do test/cases/test_case.rb:33: def capture_sql test/cases/test_case.rb:41: capture_sql { yield } ```
2019-05-21 14:25:14 +00:00
SQLCounter.log.dup
end
def assert_sql(*patterns_to_match, &block)
capture_sql(&block)
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(model = ActiveRecord::Base)
old = model.has_many_inversing
model.has_many_inversing = true
yield
ensure
model.has_many_inversing = old
if model != ActiveRecord::Base && !old
model.singleton_class.remove_method(:has_many_inversing) # reset the class_attribute
end
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
Active Record + PostgreSQL: native support for `timestamp with time zone` In https://github.com/rails/rails/issues/21126 it was suggested to make "timestamp with time zone" the default type for datetime columns in PostgreSQL. This is in line with PostgreSQL [best practices](https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_timestamp_.28without_time_zone.29). This PR lays some groundwork for that. This PR adds a configuration option, `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type`. The default is `:timestamp` which preserves current Rails behavior of using "timestamp without time zone" when you do `t.datetime` in a migration. If you change it to `:timestamptz`, you'll get "timestamp with time zone" columns instead. If you change this setting in an existing app, you should immediately call `bin/rails db:migrate` to ensure your `schema.rb` file remains correct. If you do so, then existing columns will not be impacted, so for example if you have an app with a mixture of both types of columns, and you change the config, schema dumps will continue to output the correct types. This PR also adds two new types that can be used in migrations: `t.timestamp` and `t.timestamptz`. ```ruby ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamp # default value is :timestamp create_table("foo1") do |t| t.datetime :default_format # "timestamp without time zone" t.timestamp :without_time_zone # "timestamp without time zone" t.timestamptz :with_time_zone # "timestamp with time zone" end ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz create_table("foo2") do |t| t.datetime :default_format # "timestamp with time zone" <-- note how this has changed! t.timestamp :without_time_zone # "timestamp without time zone" t.timestamptz :with_time_zone # "timestamp with time zone" end ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:my_custom_type] = { name: "custom_datetime_format_i_invented" } ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :my_custom_type create_table("foo3") do |t| t.datetime :default_format # "custom_datetime_format_i_invented" t.timestamp :without_time_zone # "timestamp without time zone" t.timestamptz :with_time_zone # "timestamp with time zone" end ``` **Notes** - This PR doesn't change the default `datetime` format. The default is still "timestamp without time zone". A future PR could do that, but there was enough code here just getting the config option right. - See also https://github.com/rails/rails/pull/41395 which set some groundwork (and added some tests) for this. - This reverts some of https://github.com/rails/rails/pull/15184. https://github.com/rails/rails/pull/15184 alluded to issues in XML serialization, but I couldn't find any related tests that this broke.
2021-03-09 19:29:07 +00:00
def with_postgresql_datetime_type(type)
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
adapter.remove_instance_variable(:@native_database_types) if adapter.instance_variable_defined?(:@native_database_types)
datetime_type_was = adapter.datetime_type
adapter.datetime_type = type
yield
ensure
adapter = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
adapter.datetime_type = datetime_type_was
adapter.remove_instance_variable(:@native_database_types) if adapter.instance_variable_defined?(:@native_database_types)
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