Skip to content

Commit f8d74e2

Browse files
authored
Merge pull request rails#53139 from mjcheetham/db-transactions
Add support for disabling transactions per database in `ActiveRecord`
2 parents e0da79a + c68482f commit f8d74e2

File tree

4 files changed

+191
-2
lines changed

4 files changed

+191
-2
lines changed

activerecord/CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
* Add support for enabling or disabling transactional tests per database.
2+
3+
A test class can now override the default `use_transactional_tests` setting
4+
for individual databases, which can be useful if some databases need their
5+
current state to be accessible to an external process while tests are running.
6+
7+
```ruby
8+
class MostlyTransactionalTest < ActiveSupport::TestCase
9+
self.use_transactional_tests = true
10+
skip_transactional_tests_for_database :shared
11+
end
12+
```
13+
14+
*Matthew Cheetham*, *Morgan Mareve*
15+
116
* Cast `query_cache` value when using URL configuration.
217

318
*zzak*

activerecord/lib/active_record/test_fixtures.rb

+27-2
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,26 @@ def after_teardown # :nodoc:
3636
class_attribute :pre_loaded_fixtures, default: false
3737
class_attribute :lock_threads, default: true
3838
class_attribute :fixture_sets, default: {}
39+
class_attribute :database_transactions_config, default: {}
3940

4041
ActiveSupport.run_load_hooks(:active_record_fixtures, self)
4142
end
4243

4344
module ClassMethods
45+
# Do not use transactional tests for the given database. This overrides
46+
# the default setting as defined by `use_transactional_tests`, which
47+
# applies to all database connection pools not explicitly configured here.
48+
def skip_transactional_tests_for_database(database_name)
49+
use_transactional_tests_for_database(database_name, false)
50+
end
51+
52+
# Enable or disable transactions per database. This overrides the default
53+
# setting as defined by `use_transactional_tests`, which applies to all
54+
# database connection pools not explicitly configured here.
55+
def use_transactional_tests_for_database(database_name, enabled = true)
56+
self.database_transactions_config = database_transactions_config.merge(database_name => enabled)
57+
end
58+
4459
# Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
4560
#
4661
# Examples:
@@ -106,7 +121,8 @@ def fixture(fixture_set_name, *fixture_names)
106121

107122
private
108123
def run_in_transaction?
109-
use_transactional_tests &&
124+
has_explicit_config = database_transactions_config.any? { |_, enabled| enabled }
125+
(use_transactional_tests || has_explicit_config) &&
110126
!self.class.uses_transaction?(name)
111127
end
112128

@@ -169,11 +185,19 @@ def invalidate_already_loaded_fixtures
169185
@@already_loaded_fixtures.clear
170186
end
171187

188+
def transactional_tests_for_pool?(pool)
189+
database_transactions_config.fetch(pool.db_config.name.to_sym, use_transactional_tests)
190+
end
191+
172192
def setup_transactional_fixtures
173193
setup_shared_connection_pool
174194

175195
# Begin transactions for connections already established
176196
@fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
197+
198+
# Filter to pools that want to use transactions
199+
@fixture_connection_pools.select! { |pool| transactional_tests_for_pool?(pool) }
200+
177201
@fixture_connection_pools.each do |pool|
178202
pool.pin_connection!(lock_threads)
179203
pool.lease_connection
@@ -189,7 +213,8 @@ def setup_transactional_fixtures
189213
if pool
190214
setup_shared_connection_pool
191215

192-
unless @fixture_connection_pools.include?(pool)
216+
# Don't begin a transaction if we've already done so, or are not using them for this pool
217+
if !@fixture_connection_pools.include?(pool) && transactional_tests_for_pool?(pool)
193218
pool.pin_connection!(lock_threads)
194219
pool.lease_connection
195220
@fixture_connection_pools << pool

activerecord/test/cases/fixtures_test.rb

+21
Original file line numberDiff line numberDiff line change
@@ -1080,13 +1080,20 @@ def connect!; end
10801080
end.new
10811081

10821082
pool = connection.pool = Class.new do
1083+
attr_accessor :db_config
1084+
10831085
def initialize(connection); @connection = connection; end
10841086
def lease_connection; @connection; end
10851087
def release_connection; end
10861088
def pin_connection!(_); end
10871089
def unpin_connection!; @connection.rollback_transaction; true; end
10881090
end.new(connection)
10891091

1092+
connection.pool.db_config = Class.new do
1093+
attr_accessor :name
1094+
def initialize(name); @name = name; end
1095+
end.new("database_name")
1096+
10901097
assert_called_with(pool, :pin_connection!, [true]) do
10911098
fire_connection_notification(connection.pool)
10921099
end
@@ -1107,13 +1114,20 @@ def connect!; end
11071114
end.new
11081115

11091116
connection.pool = Class.new do
1117+
attr_accessor :db_config
1118+
11101119
def initialize(connection); @connection = connection; end
11111120
def lease_connection; @connection; end
11121121
def release_connection; end
11131122
def pin_connection!(_); end
11141123
def unpin_connection!; @connection.rollback_transaction; true; end
11151124
end.new(connection)
11161125

1126+
connection.pool.db_config = Class.new do
1127+
attr_accessor :name
1128+
def initialize(name); @name = name; end
1129+
end.new("database_name")
1130+
11171131
fire_connection_notification(connection.pool)
11181132
teardown_fixtures
11191133

@@ -1131,13 +1145,20 @@ def connect!; end
11311145
end.new
11321146

11331147
connection.pool = Class.new do
1148+
attr_accessor :db_config
1149+
11341150
def initialize(connection); @connection = connection; end
11351151
def lease_connection; @connection; end
11361152
def release_connection; end
11371153
def pin_connection!(_); end
11381154
def unpin_connection!; @connection.rollback_transaction; true; end
11391155
end.new(connection)
11401156

1157+
connection.pool.db_config = Class.new do
1158+
attr_accessor :name
1159+
def initialize(name); @name = name; end
1160+
end.new("database_name")
1161+
11411162
assert_called_with(connection.pool, :pin_connection!, [true]) do
11421163
fire_connection_notification(connection.pool, shard: :shard_two)
11431164
end

activerecord/test/cases/test_fixtures_test.rb

+128
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,133 @@ def test_run_successfully
7070
clean_up_connection_handler
7171
FileUtils.rm_r(tmp_dir)
7272
end
73+
74+
def test_transactional_tests_per_db_explicitly_disabled
75+
tmp_dir = Dir.mktmpdir
76+
File.write(File.join(tmp_dir, "zines.yml"), <<~YML)
77+
going_out:
78+
title: Hello
79+
YML
80+
81+
klass = Class.new(Minitest::Test) do
82+
include ActiveRecord::TestFixtures
83+
84+
self.fixture_paths = [tmp_dir]
85+
self.use_transactional_tests = true
86+
self.skip_transactional_tests_for_database :primary
87+
88+
fixtures :all
89+
90+
def test_run_successfully
91+
assert_equal("Hello", Zine.first.title)
92+
assert_equal("Hello", zines(:going_out).title)
93+
# Change the data in the primary connection
94+
Zine.first.update!(title: "Goodbye")
95+
end
96+
end
97+
98+
test_result = klass.new("test_run_successfully").run
99+
assert_predicate(test_result, :passed?)
100+
# Ensure that the primary connection was NOT rolled back
101+
assert_equal("Goodbye", Zine.first.title)
102+
ensure
103+
FileUtils.rm_r(tmp_dir)
104+
end
105+
106+
def test_transactional_tests_per_db_explicitly_enabled
107+
tmp_dir = Dir.mktmpdir
108+
File.write(File.join(tmp_dir, "zines.yml"), <<~YML)
109+
going_out:
110+
title: Hello
111+
YML
112+
113+
klass = Class.new(Minitest::Test) do
114+
include ActiveRecord::TestFixtures
115+
116+
self.fixture_paths = [tmp_dir]
117+
self.use_transactional_tests = false
118+
self.use_transactional_tests_for_database :primary
119+
120+
fixtures :all
121+
122+
def test_run_successfully
123+
assert_equal("Hello", Zine.first.title)
124+
assert_equal("Hello", zines(:going_out).title)
125+
# Change the data in the primary connection
126+
Zine.first.update!(title: "Goodbye")
127+
end
128+
end
129+
130+
test_result = klass.new("test_run_successfully").run
131+
assert_predicate(test_result, :passed?)
132+
# Ensure that the primary connection WAS rolled back
133+
assert_equal("Hello", Zine.first.title)
134+
ensure
135+
FileUtils.rm_r(tmp_dir)
136+
end
137+
138+
def test_transactional_tests_per_db_default_enabled
139+
tmp_dir = Dir.mktmpdir
140+
File.write(File.join(tmp_dir, "zines.yml"), <<~YML)
141+
going_out:
142+
title: Hello
143+
YML
144+
145+
klass = Class.new(Minitest::Test) do
146+
include ActiveRecord::TestFixtures
147+
148+
self.fixture_paths = [tmp_dir]
149+
self.use_transactional_tests = true
150+
self.skip_transactional_tests_for_database :unrelated
151+
152+
fixtures :all
153+
154+
def test_run_successfully
155+
assert_equal("Hello", Zine.first.title)
156+
assert_equal("Hello", zines(:going_out).title)
157+
# Change the data in the primary connection
158+
Zine.first.update!(title: "Goodbye")
159+
end
160+
end
161+
162+
test_result = klass.new("test_run_successfully").run
163+
assert_predicate(test_result, :passed?)
164+
# Ensure that the primary connection WAS rolled back
165+
assert_equal("Hello", Zine.first.title)
166+
ensure
167+
FileUtils.rm_r(tmp_dir)
168+
end
169+
170+
def test_transactional_tests_per_db_default_disabled
171+
tmp_dir = Dir.mktmpdir
172+
File.write(File.join(tmp_dir, "zines.yml"), <<~YML)
173+
going_out:
174+
title: Hello
175+
YML
176+
177+
klass = Class.new(Minitest::Test) do
178+
include ActiveRecord::TestFixtures
179+
180+
self.fixture_paths = [tmp_dir]
181+
self.use_transactional_tests = false
182+
self.use_transactional_tests_for_database :unrelated
183+
184+
fixtures :all
185+
186+
def test_run_successfully
187+
assert_equal("Hello", Zine.first.title)
188+
assert_equal("Hello", zines(:going_out).title)
189+
# Change the data in the primary connection
190+
Zine.first.update!(title: "Goodbye")
191+
end
192+
end
193+
194+
test_result = klass.new("test_run_successfully").run
195+
assert_predicate(test_result, :passed?)
196+
# Ensure that the primary connection was NOT rolled back
197+
assert_equal("Goodbye", Zine.first.title)
198+
ensure
199+
FileUtils.rm_r(tmp_dir)
200+
end
73201
end
74202
end

0 commit comments

Comments
 (0)