Skip to content

Commit a7c1d54

Browse files
committed
Add mutant DB parallelism
1 parent 45d1956 commit a7c1d54

1 file changed

Lines changed: 87 additions & 3 deletions

File tree

core/spec/mutant_hooks.rb

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,93 @@
22

33
# Mutant hooks (see core/.mutant.yml).
44
#
5-
# The DummyApp boots with `config.eager_load = false`, so Zeitwerk loads classes
6-
# lazily. Mutant discovers subjects from loaded constants, so without forcing
7-
# eager loading the subject expressions can silently match nothing.
5+
# Per the mutant Rails guide (docs/rails.md), parallel workers must each get
6+
# their own database or they corrupt each other's fixtures. This is the
7+
# PostgreSQL pattern: each worker clones the migrated test database via
8+
# `CREATE DATABASE ... TEMPLATE`, named "<db>_mutant_worker_<index>".
9+
#
10+
# The guide's `with_root_connection` uses `ActiveRecord::Base.postgresql_connection`,
11+
# which was removed in Rails 8.x; we open the maintenance connection with the pg
12+
# gem directly instead. The rest follows the guide.
13+
require "pg"
14+
15+
# Eager load so subjects are discoverable (Zeitwerk lazy-loads otherwise).
816
hooks.register(:env_infection_post) do
917
Rails.application.eager_load!
1018
end
19+
20+
# Disconnect the parent before workers clone the template database.
21+
hooks.register(:setup_integration_post) do
22+
base_records.each do |base|
23+
disconnect_pool(base:)
24+
end
25+
end
26+
27+
# Both registrations are required to isolate in both modes:
28+
# mutation_worker_process_start for `mutant run`, test_worker_process_start for `mutant test`.
29+
hooks.register(:test_worker_process_start) { |index:| isolate_index(index:) }
30+
hooks.register(:mutation_worker_process_start) { |index:| isolate_index(index:) }
31+
32+
def self.base_records
33+
[
34+
ActiveRecord::Base,
35+
]
36+
end
37+
38+
def self.isolate_index(index:)
39+
base_records.each do |base|
40+
disconnect_pool(base:)
41+
isolate_database(base:, index:)
42+
end
43+
end
44+
45+
def self.isolate_database(base:, index:)
46+
db_config = base
47+
.connection_handler
48+
.retrieve_connection_pool(base.connection_specification_name)
49+
.db_config
50+
51+
raw_template_database = db_config.database
52+
raw_isolated_database = "#{raw_template_database}_mutant_worker_#{index}"
53+
54+
with_root_connection do |connection|
55+
template_database = PG::Connection.quote_ident(raw_template_database)
56+
isolated_database = PG::Connection.quote_ident(raw_isolated_database)
57+
58+
connection.exec("DROP DATABASE IF EXISTS #{isolated_database}")
59+
connection.exec("CREATE DATABASE #{isolated_database} TEMPLATE #{template_database}")
60+
end
61+
62+
db_config._database = raw_isolated_database
63+
end
64+
65+
def self.disconnect_pool(base:)
66+
base
67+
.connection_handler
68+
.retrieve_connection_pool(base.connection_specification_name)
69+
.disconnect
70+
end
71+
72+
# Open a connection to the "postgres" maintenance database so we can issue
73+
# CREATE/DROP DATABASE. (Replaces the guide's removed Base.postgresql_connection.)
74+
def self.with_root_connection
75+
base = ActiveRecord::Base
76+
77+
config = base
78+
.connection_handler
79+
.retrieve_connection_pool(base.connection_specification_name)
80+
.db_config
81+
.configuration_hash
82+
83+
connection = PG.connect(
84+
host: config[:host],
85+
port: config[:port] || 5432,
86+
user: config[:username],
87+
password: config[:password],
88+
dbname: "postgres"
89+
)
90+
91+
yield connection
92+
ensure
93+
connection&.close
94+
end

0 commit comments

Comments
 (0)