File: test_case.rb

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (332 lines) | stat: -rw-r--r-- 11,608 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# frozen_string_literal: true

require "active_support/testing/strict_warnings"
require "active_support"
require "active_support/testing/autorun"
require "active_support/testing/method_call_assertions"
require "active_support/testing/stream"
require "active_record/testing/query_assertions"
require "active_record/fixtures"

require "cases/validations_repair_helper"
require_relative "../support/config"
require_relative "../support/connection"
require_relative "../support/adapter_helper"
require_relative "../support/load_schema_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::Assertions::QueryAssertions
    include ActiveRecord::TestFixtures
    include ActiveRecord::ValidationsRepairHelper
    include AdapterHelper
    extend AdapterHelper
    include LoadSchemaHelper
    extend LoadSchemaHelper

    self.fixture_paths = [FIXTURES_ROOT]
    self.use_instantiated_fixtures = false
    self.use_transactional_tests = true

    def after_teardown
      super
      check_connection_leaks
    end

    def check_connection_leaks
      return if in_memory_db?

      # Make sure tests didn't leave a connection owned by some background thread
      # which could lead to some slow wait in a subsequent thread.
      leaked_conn = []
      ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
        # Ensure all in flights tasks are completed.
        # Otherwise they may still hold a connection.
        if pool.async_executor
          if pool.async_executor.scheduled_task_count != pool.async_executor.completed_task_count
            pool.connections.each do |conn|
              if conn.in_use? && conn.owner != Fiber.current && conn.owner != Thread.current
                if conn.owner.respond_to?(:join)
                  conn.owner&.join(0.5)
                end
              end
            end
          end
        end

        pool.reap
        pool.connections.each do |conn|
          if conn.in_use?
            if conn.owner != Fiber.current && conn.owner != Thread.current
              leaked_conn << [conn.owner, conn.owner.backtrace]
              conn.owner&.kill
            end
            conn.steal!
            pool.checkin(conn)
          end
        end
      end

      if leaked_conn.size > 0
        puts "Found #{leaked_conn.size} leaked connections"
        leaked_conn.each do |owner, backtrace|
          puts "owner: #{owner}"
          puts "backtrace:\n#{backtrace}"
          puts
        end
        raise "Found #{leaked_conn.size} leaked connection after #{self.class.name}##{name}"
      end
    end

    def create_fixtures(*fixture_set_names)
      ActiveRecord::FixtureSet.create_fixtures(ActiveRecord::TestCase.fixture_paths, fixture_set_names, fixture_class_names)
    end

    def capture_sql(include_schema: false)
      counter = SQLCounter.new
      ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
        yield
        if include_schema
          counter.log_all
        else
          counter.log
        end
      end
    end

    def capture_sql_and_binds
      counter = SQLCounter.new
      ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
        yield
        counter.log_full
      end
    end

    # Redefine existing assertion method to explicitly not materialize transactions.
    def assert_queries_match(match, count: nil, include_schema: false, &block)
      counter = SQLCounter.new
      ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
        result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
        queries = include_schema ? counter.log_all : counter.log
        matched_queries = queries.select { |query| match === query }

        if count
          assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
        else
          assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
        end

        result
      end
    end

    def assert_column(model, column_name, msg = nil)
      model.reset_column_information
      assert_includes model.column_names, column_name.to_s, msg
    end

    def assert_no_column(model, column_name, msg = nil)
      model.reset_column_information
      assert_not_includes model.column_names, column_name.to_s, msg
    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 with_automatic_scope_inversing(*reflections)
      old = reflections.map { |reflection| reflection.klass.automatic_scope_inversing }

      reflections.each do |reflection|
        reflection.klass.automatic_scope_inversing = true
        reflection.remove_instance_variable(:@inverse_name) if reflection.instance_variable_defined?(:@inverse_name)
        reflection.remove_instance_variable(:@inverse_of) if reflection.instance_variable_defined?(:@inverse_of)
      end

      yield
    ensure
      reflections.each_with_index do |reflection, i|
        reflection.klass.automatic_scope_inversing = old[i]
        reflection.remove_instance_variable(:@inverse_name) if reflection.instance_variable_defined?(:@inverse_name)
        reflection.remove_instance_variable(:@inverse_of) if reflection.instance_variable_defined?(:@inverse_of)
      end
    end

    def with_db_warnings_action(action, warnings_to_ignore = [])
      original_db_warnings_ignore = ActiveRecord.db_warnings_ignore

      ActiveRecord.db_warnings_action = action
      ActiveRecord.db_warnings_ignore = warnings_to_ignore

      ActiveRecord::Base.lease_connection.disconnect! # Disconnect from the db so that we reconfigure the connection

      yield
    ensure
      ActiveRecord.db_warnings_action = @original_db_warnings_action
      ActiveRecord.db_warnings_ignore = original_db_warnings_ignore
      ActiveRecord::Base.lease_connection.disconnect!
    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

    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

    def with_env_tz(new_tz = "US/Eastern")
      old_tz, ENV["TZ"] = ENV["TZ"], new_tz
      yield
    ensure
      old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ")
    end

    def with_timezone_config(cfg)
      verify_default_timezone_config

      old_default_zone = ActiveRecord.default_timezone
      old_awareness = ActiveRecord::Base.time_zone_aware_attributes
      old_aware_types = ActiveRecord::Base.time_zone_aware_types
      old_zone = Time.zone

      if cfg.has_key?(:default)
        ActiveRecord.default_timezone = cfg[:default]
      end
      if cfg.has_key?(:aware_attributes)
        ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes]
      end
      if cfg.has_key?(:aware_types)
        ActiveRecord::Base.time_zone_aware_types = cfg[:aware_types]
      end
      if cfg.has_key?(:zone)
        Time.zone = cfg[:zone]
      end
      yield
    ensure
      ActiveRecord.default_timezone = old_default_zone
      ActiveRecord::Base.time_zone_aware_attributes = old_awareness
      ActiveRecord::Base.time_zone_aware_types = old_aware_types
      Time.zone = old_zone
    end

    # This method makes sure that tests don't leak global state related to time zones.
    EXPECTED_ZONE = nil
    EXPECTED_DEFAULT_TIMEZONE = :utc
    EXPECTED_AWARE_TYPES = [:datetime, :time]
    EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
    def verify_default_timezone_config
      if Time.zone != EXPECTED_ZONE
        $stderr.puts <<-MSG
    \n#{self}
        Global state `Time.zone` was leaked.
          Expected: #{EXPECTED_ZONE}
          Got: #{Time.zone}
        MSG
      end
      if ActiveRecord.default_timezone != EXPECTED_DEFAULT_TIMEZONE
        $stderr.puts <<-MSG
    \n#{self}
        Global state `ActiveRecord.default_timezone` was leaked.
          Expected: #{EXPECTED_DEFAULT_TIMEZONE}
          Got: #{ActiveRecord.default_timezone}
        MSG
      end
      if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
        $stderr.puts <<-MSG
    \n#{self}
        Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
          Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
          Got: #{ActiveRecord::Base.time_zone_aware_attributes}
        MSG
      end
      if ActiveRecord::Base.time_zone_aware_types != EXPECTED_AWARE_TYPES
        $stderr.puts <<-MSG
    \n#{self}
        Global state `ActiveRecord::Base.time_zone_aware_types` was leaked.
          Expected: #{EXPECTED_AWARE_TYPES}
          Got: #{ActiveRecord::Base.time_zone_aware_types}
        MSG
      end
    end

    def clean_up_connection_handler
      handler = ActiveRecord::Base.connection_handler
      handler.instance_variable_get(:@connection_name_to_pool_manager).each do |owner, pool_manager|
        pool_manager.role_names.each do |role_name|
          next if role_name == ActiveRecord::Base.default_role &&
                  # TODO: Remove this helper when `remove_connection` for different shards is fixed.
                  # See https://github.com/rails/rails/pull/49382.
                  ["ActiveRecord::Base", "ARUnit2Model", "Contact", "ContactSti"].include?(owner)
          pool_manager.remove_role(role_name)
        end
      end
    end

    # Connect to the database
    ARTest.connect
    # Load database schema
    load_schema
  end

  class PostgreSQLTestCase < TestCase
    def self.run(*args)
      super if current_adapter?(:PostgreSQLAdapter)
    end
  end

  class AbstractMysqlTestCase < TestCase
    def self.run(*args)
      super if current_adapter?(:Mysql2Adapter) || current_adapter?(:TrilogyAdapter)
    end
  end

  class Mysql2TestCase < TestCase
    def self.run(*args)
      super if current_adapter?(:Mysql2Adapter)
    end
  end

  class TrilogyTestCase < TestCase
    def self.run(*args)
      super if current_adapter?(:TrilogyAdapter)
    end
  end


  class SQLite3TestCase < TestCase
    def self.run(*args)
      super if current_adapter?(:SQLite3Adapter)
    end
  end
end