Files
fixin.me/lib/tasks/test_databases.rake
barbie-bot badb4b336c Use DATABASE_URL instead of temp database.yml for multi-db tests
Replace the RAILS_DATABASE_YML / Dir.mktmpdir approach with DATABASE_URL:
- Read database config via Rails.application.config.database_configuration
  instead of manually parsing YAML+ERB
- Build a DATABASE_URL from each test config and pass it to subprocesses;
  Rails merges it on top of the test: entry — no temp files needed
- Remove non_test_configs, Dir.mktmpdir, and the require yaml/erb/tmpdir
- Remove RAILS_DATABASE_YML override from config/application.rb(.dist)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:47:35 +00:00

165 lines
5.3 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
require 'uri'
# Multi-database test runner
# ==========================
# When database.yml contains more than one `test*` configuration, every
# standard test task (test, test:models, test:system, …) is automatically
# rewritten to run the full suite against EACH configured database in turn.
#
# Convention — any top-level key that starts with "test" and holds a Hash:
#
# test: ← required primary database
# adapter: mysql2
# database: fixinme_test
# ...
#
# test_sqlite: ← optional additional databases
# adapter: sqlite3
# database: db/fixinme_test.sqlite3
#
# test_pg:
# adapter: postgresql
# ...
#
# A single-database setup is unchanged: every task behaves exactly as before.
#
# Database configuration is read via Rails.application.config.database_configuration
# so ERB interpolation and all Rails path overrides are respected automatically.
# Each subprocess receives DATABASE_URL built from the selected config, which
# Rails merges on top of the test: entry from database.yml — no temporary files needed.
module MultiDbTests
ADAPTER_GEMS = {
'mysql2' => 'mysql2',
'sqlite3' => 'sqlite3',
'postgresql' => 'pg',
'pg' => 'pg',
}.freeze
ADAPTER_BUNDLE_GROUPS = {
'mysql2' => 'mysql',
'sqlite3' => 'sqlite',
'postgresql' => 'postgresql',
'pg' => 'postgresql',
}.freeze
# Rake task names generated by railties/lib/rails/test_unit/testing.rake
# that use run_from_rake — these are the ones we rewrite.
WRAPPED_TASKS = (
['test'] +
Rails::TestUnit::Runner::TEST_FOLDERS.map { |f| "test:#{f}" } +
%w[test:all test:system test:generators test:units test:functionals]
).freeze
class << self
# Returns {name => config_hash} for every key starting with "test".
def test_configs
@test_configs ||= Rails.application.config.database_configuration
.select { |k, v| k.to_s.start_with?('test') && v.is_a?(Hash) }
end
# Run rails +task_name+ for every configured test database.
# Called from the rewritten rake task actions.
def run(task_name)
cfgs = test_configs
results = {}
cfgs.each do |db_name, config|
adapter = config['adapter'].to_s
puts "\n#{'─' * 64}"
puts " #{task_name} · #{db_name} (#{adapter})"
puts '─' * 64
unless adapter_available?(adapter)
warn " SKIPPED — '#{adapter}' gem not in bundle.\n" \
" Run: bundle config --local with \"#{current_with} #{adapter_group(adapter)}\""
results[db_name] = :skipped
next
end
env = { 'DATABASE_URL' => config_to_url(config) }
if system(env, 'bundle exec rails db:test:prepare')
results[db_name] = system(env, 'rails', task_name) ? :pass : :fail
else
results[db_name] = :prepare_failed
end
end
print_summary(cfgs, results)
failed = results.count { |_, s| [:fail, :prepare_failed].include?(s) }
exit(false) if failed > 0
end
private
# Build a DATABASE_URL string from a database.yml config hash.
# Rails merges DATABASE_URL on top of the test: entry, so adapter and
# connection details from the URL take precedence over the YAML file.
def config_to_url(config)
adapter = config['adapter'].to_s
database = config['database'].to_s
return "sqlite3:#{database}" if adapter == 'sqlite3'
user = config['username']
pass = config['password']
host = config['host'] || 'localhost'
port = config['port']
socket = config['socket']
userinfo = user ? "#{URI.encode_www_form_component(user)}:#{URI.encode_www_form_component(pass.to_s)}@" : ''
hostport = port ? "#{host}:#{port}" : host
url = "#{adapter}://#{userinfo}#{hostport}/#{database}"
url += "?socket=#{URI.encode_www_form_component(socket)}" if socket
url
end
def adapter_available?(adapter)
require ADAPTER_GEMS.fetch(adapter, adapter)
true
rescue LoadError
false
end
def adapter_group(adapter)
ADAPTER_BUNDLE_GROUPS.fetch(adapter, adapter)
end
def current_with
(Bundler.settings[:with] || '').split(':').join(' ')
end
def print_summary(cfgs, results)
return if results.size <= 1 # no summary for single-DB runs
puts "\n#{'═' * 64}"
puts ' SUMMARY'
puts '═' * 64
results.each do |db_name, status|
adapter = cfgs.dig(db_name, 'adapter') || '?'
icon = status == :pass ? '✓' : (status == :skipped ? '' : '✗')
label = { pass: 'PASS', fail: 'FAIL',
prepare_failed: 'PREPARE FAILED', skipped: 'SKIPPED' }[status]
puts " #{icon} #{db_name.ljust(26)} (#{adapter.ljust(12)}) #{label}"
end
puts '═' * 64
end
end
end
# Rewrite every standard test task to run against all configured databases.
# This file loads after railties/testing.rake, so all tasks already exist.
# Single-database setups are completely unaffected.
if MultiDbTests.test_configs.size > 1
MultiDbTests::WRAPPED_TASKS.each do |task_name|
next unless Rake::Task.task_defined?(task_name)
Rake::Task[task_name].clear_actions
Rake::Task[task_name].enhance do
MultiDbTests.run(task_name)
end
end
end