require 'yaml' require 'erb' require 'tmpdir' namespace :test do desc <<~DESC Run the full test suite against every test database configured in database.yml. Any top-level key that starts with "test" and contains a Hash is treated as a test database configuration: test: # always required — the primary test database adapter: mysql2 database: fixinme_test ... test_sqlite: # optional additional databases adapter: sqlite3 database: db/fixinme_test.sqlite3 test_pg: adapter: postgresql database: fixinme_test ... For each database the task will: 1. Check that the required adapter gem is available (skip with warning if not). 2. Run `rails db:test:prepare` to create/migrate the database. 3. Run `rails test` and record pass/fail. A summary is printed at the end. The task exits non-zero if any database fails. DESC task :all_databases do db_file = Rails.root.join('config', 'database.yml') all = YAML.safe_load(ERB.new(db_file.read).result, aliases: true) || {} test_cfgs = all.select { |k, v| k.to_s.start_with?('test') && v.is_a?(Hash) } non_test = all.reject { |k, _| k.to_s.start_with?('test') } abort "No test database configurations found in #{db_file}." if test_cfgs.empty? results = {} test_cfgs.each do |name, config| adapter = config['adapter'].to_s puts "\n#{'─' * 64}" puts " #{name} (adapter: #{adapter})" puts '─' * 64 unless adapter_available?(adapter) puts " SKIPPED — gem for '#{adapter}' adapter not available in bundle." puts " Add it to Gemfile (e.g. `bundle config --local with #{adapter_group(adapter)}`)" results[name] = :skipped next end Dir.mktmpdir('rails_test_db_') do |tmpdir| tmp_yml = File.join(tmpdir, 'database.yml') # Write a standalone database.yml with just this config as `test:` # (non-test configs are preserved so boot doesn't fail on `production:` lookup) File.write(tmp_yml, non_test.merge('test' => config).to_yaml) env = { 'RAILS_DATABASE_YML' => tmp_yml } if system(env, 'bundle exec rails db:test:prepare') results[name] = system(env, 'bundle exec rails test') ? :pass : :fail else results[name] = :prepare_failed end end end puts "\n#{'═' * 64}" puts " SUMMARY" puts '═' * 64 results.each do |name, status| adapter = test_cfgs[name]['adapter'] icon = { pass: '✓', fail: '✗', prepare_failed: '✗', skipped: '–' }[status] label = { pass: 'PASS', fail: 'FAIL', prepare_failed: 'PREPARE FAILED', skipped: 'SKIPPED' }[status] puts " #{icon} #{name.ljust(26)} (#{adapter.ljust(12)}) #{label}" end puts '═' * 64 failed = results.count { |_, s| s == :fail || s == :prepare_failed } abort "\n #{failed} database(s) failed." if failed > 0 end end private # Returns true if the gem required for this adapter is loadable. ADAPTER_GEMS = { 'mysql2' => 'mysql2', 'sqlite3' => 'sqlite3', 'postgresql' => 'pg', 'pg' => 'pg', }.freeze def adapter_available?(adapter) gem_name = ADAPTER_GEMS.fetch(adapter, adapter) require gem_name true rescue LoadError false end # Returns the Bundler group name that installs the adapter gem. ADAPTER_GROUPS = { 'mysql2' => 'mysql', 'sqlite3' => 'sqlite', 'postgresql' => 'postgresql', 'pg' => 'postgresql', }.freeze def adapter_group(adapter) ADAPTER_GROUPS.fetch(adapter, adapter) end