Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ to open an issue or a PR with the fix.
ACTIVE_RECORD_DOCTOR_DEBUG=1 bundle exec rake active_record_doctor
```

### Application Record base class

Rails applications often use `ApplicationRecord` instead of inheriting directly from `ActiveRecord::Base`. Detectors resolve models and connections from a configurable root class:

- **`ACTIVE_RECORD_DOCTOR_BASE_CLASS`** — constant name (e.g. `ApplicationRecord`). Defaults to `ActiveRecord::Base` when unset. Useful when multiple abstract roots exist (for example multi-database setups) so checks use the intended connection and model hierarchy.

Example:

```
ACTIVE_RECORD_DOCTOR_BASE_CLASS=ApplicationRecord bundle exec rake active_record_doctor
```

### Configuration

`active_record_doctor` can be configured to better suit your project's needs.
Expand Down
2 changes: 2 additions & 0 deletions active_record_doctor.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Gem::Specification.new do |s|

s.add_dependency "activerecord", ACTIVE_RECORD_SPEC

# minitest-fork_executor 1.x still hooks Minitest.run_one_method (removed in Minitest 6).
s.add_development_dependency "minitest", ">= 5.1", "< 6"
s.add_development_dependency "minitest-fork_executor", "~> 1.0.2"
s.add_development_dependency "mysql2", "~> 0.5.6"
s.add_development_dependency "pg", "~> 1.5.9"
Expand Down
7 changes: 4 additions & 3 deletions lib/active_record_doctor/detectors/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ def locals_and_globals
end
end

def initialize(config:, logger:, io:)
def initialize(config:, logger:, base_class:, io:)
@problems = []
@config = config
@logger = logger
@base_class = base_class
@io = io
end

Expand Down Expand Up @@ -110,7 +111,7 @@ def warning(message)
end

def connection
@connection ||= ActiveRecord::Base.connection
@connection ||= @base_class.connection
end

def indexes(table_name)
Expand Down Expand Up @@ -156,7 +157,7 @@ def check_constraints(table_name)
end

def models
ActiveRecord::Base.descendants.sort_by(&:name)
@base_class.descendants.sort_by(&:name)
end

def underscored_name
Expand Down
6 changes: 5 additions & 1 deletion lib/active_record_doctor/rake/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,18 @@ def define
private

def runner
@runner ||= ActiveRecordDoctor::Runner.new(config: config, logger: logger)
@runner ||= ActiveRecordDoctor::Runner.new(config: config, logger: logger, base_class: base_class)
end

def config
@config ||=
ActiveRecordDoctor.load_config_with_defaults(effective_config_path)
end

def base_class
@base_class ||= ENV.fetch("ACTIVE_RECORD_DOCTOR_BASE_CLASS", "ActiveRecord::Base").constantize
end

def effective_config_path
if config_path.nil?
# No explicit config_path was set, so we're trying to use defaults.
Expand Down
6 changes: 4 additions & 2 deletions lib/active_record_doctor/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ module ActiveRecordDoctor # :nodoc:
# and an output device for use by detectors.
class Runner
# io is injected via constructor parameters to facilitate testing.
def initialize(config:, logger:, io: $stdout)
def initialize(config:, logger:, base_class: ActiveRecord::Base, io: $stdout)
@config = config
@logger = logger
@base_class = base_class
@io = io
end

Expand All @@ -16,6 +17,7 @@ def run_one(name)
ActiveRecordDoctor.detectors.fetch(name).run(
config: config,
logger: logger,
base_class: base_class,
io: io
)
end
Expand All @@ -41,6 +43,6 @@ def help(name)

private

attr_reader :config, :logger, :io
attr_reader :config, :logger, :base_class, :io
end
end
98 changes: 98 additions & 0 deletions test/active_record_doctor/base_class_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

class ActiveRecordDoctor::BaseClassTest < Minitest::Test
def setup
@config = ActiveRecordDoctor.load_config_with_defaults(nil)
@logger = ActiveRecordDoctor::Logger::Dummy.new
@io = StringIO.new
end

def test_runner_defaults_base_class_to_active_record_base
runner = ActiveRecordDoctor::Runner.new(config: @config, logger: @logger, io: @io)

assert_equal(ActiveRecord::Base, runner.send(:base_class))
end

def test_runner_accepts_custom_base_class
runner = ActiveRecordDoctor::Runner.new(
config: @config,
logger: @logger,
base_class: ApplicationRecord,
io: @io
)

assert_equal(ApplicationRecord, runner.send(:base_class))
end

def test_detector_models_are_scoped_to_base_class
# Anonymous classes avoid polluting the global detector suite; tie to an
# existing table so other detectors are not affected by random test order.
isolated_root = Class.new(ActiveRecord::Base) do
self.abstract_class = true
end
isolated_leaf = Class.new(isolated_root) do
self.table_name = "schema_migrations"
end

detector = ActiveRecordDoctor::Detectors::MissingForeignKeys.new(
config: @config,
logger: @logger,
base_class: isolated_root,
io: @io
)

assert_includes(detector.send(:models), isolated_leaf)
refute_includes(detector.send(:models), ApplicationRecord)
end

def test_detector_connection_follows_base_class
primary = ActiveRecordDoctor::Detectors::MissingForeignKeys.new(
config: @config,
logger: @logger,
base_class: ApplicationRecord,
io: @io
)

secondary = ActiveRecordDoctor::Detectors::MissingForeignKeys.new(
config: @config,
logger: @logger,
base_class: SecondaryRecord,
io: @io
)

assert_equal(ApplicationRecord.connection, primary.send(:connection))
assert_equal(SecondaryRecord.connection, secondary.send(:connection))
end

def test_rake_task_resolves_base_class_from_environment
previous = ENV.fetch("ACTIVE_RECORD_DOCTOR_BASE_CLASS", nil)
ENV["ACTIVE_RECORD_DOCTOR_BASE_CLASS"] = "ApplicationRecord"

task = ActiveRecordDoctor::Rake::Task.new { |t| t.deps = [] }

assert_equal(ApplicationRecord, task.send(:base_class))
ensure
restore_env("ACTIVE_RECORD_DOCTOR_BASE_CLASS", previous)
end

def test_rake_task_defaults_base_class_to_active_record_base
previous = ENV.fetch("ACTIVE_RECORD_DOCTOR_BASE_CLASS", nil)
ENV.delete("ACTIVE_RECORD_DOCTOR_BASE_CLASS")

task = ActiveRecordDoctor::Rake::Task.new { |t| t.deps = [] }

assert_equal(ActiveRecord::Base, task.send(:base_class))
ensure
restore_env("ACTIVE_RECORD_DOCTOR_BASE_CLASS", previous)
end

private

def restore_env(key, previous)
if previous
ENV[key] = previous
else
ENV.delete(key)
end
end
end