active_record_doctor 1.8.0 → 1.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +246 -48
  3. data/lib/active_record_doctor/config/default.rb +59 -0
  4. data/lib/active_record_doctor/config/loader.rb +137 -0
  5. data/lib/active_record_doctor/config.rb +14 -0
  6. data/lib/active_record_doctor/detectors/base.rb +110 -19
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +63 -37
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +32 -23
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +70 -34
  10. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  11. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
  12. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +40 -28
  13. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +28 -21
  14. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +40 -30
  15. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
  16. data/lib/active_record_doctor/detectors/undefined_table_references.rb +19 -20
  17. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +44 -18
  18. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
  19. data/lib/active_record_doctor/detectors.rb +12 -4
  20. data/lib/active_record_doctor/errors.rb +226 -0
  21. data/lib/active_record_doctor/help.rb +39 -0
  22. data/lib/active_record_doctor/rake/task.rb +78 -0
  23. data/lib/active_record_doctor/runner.rb +41 -0
  24. data/lib/active_record_doctor/version.rb +1 -1
  25. data/lib/active_record_doctor.rb +7 -3
  26. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
  27. data/lib/tasks/active_record_doctor.rake +9 -18
  28. data/test/active_record_doctor/config/loader_test.rb +120 -0
  29. data/test/active_record_doctor/config_test.rb +116 -0
  30. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +131 -8
  31. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
  32. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +190 -12
  33. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  34. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
  35. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +138 -24
  36. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +74 -13
  37. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +57 -8
  38. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
  39. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
  40. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +112 -8
  41. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
  42. data/test/active_record_doctor/runner_test.rb +42 -0
  43. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  44. data/test/model_factory.rb +73 -23
  45. data/test/setup.rb +62 -72
  46. metadata +40 -9
  47. data/lib/active_record_doctor/printers/io_printer.rb +0 -133
  48. data/lib/active_record_doctor/task.rb +0 -28
  49. data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
@@ -27,10 +27,9 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
27
27
  name: "index_profiles_on_first_name_and_last_name"
28
28
  end
29
29
 
30
- assert_problems(<<OUTPUT)
31
- The following indexes should include `deleted_at IS NULL`:
32
- index_profiles_on_first_name_and_last_name
33
- OUTPUT
30
+ assert_problems(<<~OUTPUT)
31
+ consider adding `WHERE deleted_at IS NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
32
+ OUTPUT
34
33
  end
35
34
 
36
35
  def test_indexed_discarded_at_is_not_reported
@@ -59,9 +58,114 @@ OUTPUT
59
58
  name: "index_profiles_on_first_name_and_last_name"
60
59
  end
61
60
 
62
- assert_problems(<<OUTPUT)
63
- The following indexes should include `deleted_at IS NULL`:
64
- index_profiles_on_first_name_and_last_name
65
- OUTPUT
61
+ assert_problems(<<~OUTPUT)
62
+ consider adding `WHERE discarded_at IS NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
63
+ OUTPUT
64
+ end
65
+
66
+ def test_config_ignore_tables
67
+ skip("MySQL doesn't support partial indexes") if mysql?
68
+
69
+ create_table(:users) do |t|
70
+ t.string :first_name
71
+ t.string :last_name
72
+ t.datetime :discarded_at
73
+ t.index [:first_name, :last_name],
74
+ name: "index_profiles_on_first_name_and_last_name"
75
+ end
76
+
77
+ config_file(<<-CONFIG)
78
+ ActiveRecordDoctor.configure do |config|
79
+ config.detector :unindexed_deleted_at,
80
+ ignore_tables: ["users"]
81
+ end
82
+ CONFIG
83
+
84
+ refute_problems
85
+ end
86
+
87
+ def test_global_ignore_tables
88
+ skip("MySQL doesn't support partial indexes") if mysql?
89
+
90
+ create_table(:users) do |t|
91
+ t.string :first_name
92
+ t.string :last_name
93
+ t.datetime :discarded_at
94
+ t.index [:first_name, :last_name],
95
+ name: "index_profiles_on_first_name_and_last_name"
96
+ end
97
+
98
+ config_file(<<-CONFIG)
99
+ ActiveRecordDoctor.configure do |config|
100
+ config.global :ignore_tables, ["users"]
101
+ end
102
+ CONFIG
103
+
104
+ refute_problems
105
+ end
106
+
107
+ def test_config_ignore_columns
108
+ skip("MySQL doesn't support partial indexes") if mysql?
109
+
110
+ create_table(:users) do |t|
111
+ t.string :first_name
112
+ t.string :last_name
113
+ t.datetime :discarded_at
114
+ t.index [:first_name, :last_name],
115
+ name: "index_profiles_on_first_name_and_last_name"
116
+ end
117
+
118
+ config_file(<<-CONFIG)
119
+ ActiveRecordDoctor.configure do |config|
120
+ config.detector :unindexed_deleted_at,
121
+ ignore_columns: ["users.discarded_at"]
122
+ end
123
+ CONFIG
124
+
125
+ refute_problems
126
+ end
127
+
128
+ def test_config_ignore_indexes
129
+ skip("MySQL doesn't support partial indexes") if mysql?
130
+
131
+ create_table(:users) do |t|
132
+ t.string :first_name
133
+ t.string :last_name
134
+ t.datetime :discarded_at
135
+ t.index [:first_name, :last_name],
136
+ name: "index_profiles_on_first_name_and_last_name"
137
+ end
138
+
139
+ config_file(<<-CONFIG)
140
+ ActiveRecordDoctor.configure do |config|
141
+ config.detector :unindexed_deleted_at,
142
+ ignore_indexes: ["index_profiles_on_first_name_and_last_name"]
143
+ end
144
+ CONFIG
145
+
146
+ refute_problems
147
+ end
148
+
149
+ def test_config_column_names
150
+ skip("MySQL doesn't support partial indexes") if mysql?
151
+
152
+ create_table(:users) do |t|
153
+ t.string :first_name
154
+ t.string :last_name
155
+ t.datetime :obliverated_at
156
+ t.index [:first_name, :last_name],
157
+ name: "index_profiles_on_first_name_and_last_name"
158
+ end
159
+
160
+ config_file(<<-CONFIG)
161
+ ActiveRecordDoctor.configure do |config|
162
+ config.detector :unindexed_deleted_at,
163
+ column_names: ["obliverated_at"]
164
+ end
165
+ CONFIG
166
+
167
+ assert_problems(<<~OUTPUT)
168
+ consider adding `WHERE obliverated_at IS NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
169
+ OUTPUT
66
170
  end
67
171
  end
@@ -9,10 +9,9 @@ class ActiveRecordDoctor::Detectors::UnindexedForeignKeysTest < Minitest::Test
9
9
  t.references :company, foreign_key: true, index: false
10
10
  end
11
11
 
12
- assert_problems(<<OUTPUT)
13
- The following foreign keys should be indexed for performance reasons:
14
- users company_id
15
- OUTPUT
12
+ assert_problems(<<~OUTPUT)
13
+ add an index on users.company_id - foreign keys are often used in database lookups and should be indexed for performance reasons
14
+ OUTPUT
16
15
  end
17
16
 
18
17
  def test_indexed_foreign_key_is_not_reported
@@ -23,4 +22,57 @@ OUTPUT
23
22
 
24
23
  refute_problems
25
24
  end
25
+
26
+ def test_config_ignore_tables
27
+ skip("MySQL always indexes foreign keys") if mysql?
28
+
29
+ create_table(:companies)
30
+ create_table(:users) do |t|
31
+ t.references :company, foreign_key: true, index: false
32
+ end
33
+
34
+ config_file(<<-CONFIG)
35
+ ActiveRecordDoctor.configure do |config|
36
+ config.detector :unindexed_foreign_keys,
37
+ ignore_tables: ["users"]
38
+ end
39
+ CONFIG
40
+
41
+ refute_problems
42
+ end
43
+
44
+ def test_global_ignore_tables
45
+ skip("MySQL always indexes foreign keys") if mysql?
46
+
47
+ create_table(:companies)
48
+ create_table(:users) do |t|
49
+ t.references :company, foreign_key: true, index: false
50
+ end
51
+
52
+ config_file(<<-CONFIG)
53
+ ActiveRecordDoctor.configure do |config|
54
+ config.global :ignore_tables, ["users"]
55
+ end
56
+ CONFIG
57
+
58
+ refute_problems
59
+ end
60
+
61
+ def test_config_ignore_columns
62
+ skip("MySQL always indexes foreign keys") if mysql?
63
+
64
+ create_table(:companies)
65
+ create_table(:users) do |t|
66
+ t.references :company, foreign_key: true, index: false
67
+ end
68
+
69
+ config_file(<<-CONFIG)
70
+ ActiveRecordDoctor.configure do |config|
71
+ config.detector :unindexed_foreign_keys,
72
+ ignore_columns: ["users.company_id"]
73
+ end
74
+ CONFIG
75
+
76
+ refute_problems
77
+ end
26
78
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::RunnerTest < Minitest::Test
4
+ def test_run_one_raises_on_unknown_detectors
5
+ io = StringIO.new
6
+ runner = ActiveRecordDoctor::Runner.new(load_config, io)
7
+
8
+ assert_raises(KeyError) do
9
+ runner.run_one(:performance_issues)
10
+ end
11
+ end
12
+
13
+ def test_run_all_returns_true_when_no_errors
14
+ io = StringIO.new
15
+ runner = ActiveRecordDoctor::Runner.new(load_config, io)
16
+
17
+ assert(runner.run_all)
18
+ assert(io.string.blank?)
19
+ end
20
+
21
+ def test_run_all_returns_false_when_errors
22
+ # Create a model without its underlying table to trigger an error.
23
+ create_model(:User)
24
+
25
+ io = StringIO.new
26
+ runner = ActiveRecordDoctor::Runner.new(load_config, io)
27
+
28
+ refute(runner.run_all)
29
+ refute(io.string.blank?)
30
+ end
31
+
32
+ def test_help_prints_help
33
+ ActiveRecordDoctor.detectors.each do |name, _|
34
+ io = StringIO.new
35
+ runner = ActiveRecordDoctor::Runner.new(load_config, io)
36
+
37
+ runner.help(name)
38
+
39
+ refute(io.string.blank?, "expected help for #{name}")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ require "generators/active_record_doctor/add_indexes/add_indexes_generator"
6
+
7
+ class ActiveRecordDoctor::AddIndexesGeneratorTest < Minitest::Test
8
+ TIMESTAMP = Time.new(2021, 2, 1, 13, 15, 30)
9
+
10
+ def test_create_migrations
11
+ create_table(:users) do |t|
12
+ t.integer :organization_id, null: false
13
+ t.integer :account_id, null: false
14
+ end
15
+ create_table(:organizations) do |t|
16
+ t.integer :owner_id
17
+ end
18
+
19
+ Dir.mktmpdir do |dir|
20
+ Dir.chdir(dir)
21
+
22
+ path = File.join(dir, "indexes.txt")
23
+ File.write(path, <<~INDEXES)
24
+ add an index on users.organization_id - foreign keys are often used in database lookups and should be indexed for performance reasons
25
+ add an index on users.account_id - foreign keys are often used in database lookups and should be indexed for performance reasons
26
+ add an index on organizations.owner_id - foreign keys are often used in database lookups and should be indexed for performance reasons
27
+ INDEXES
28
+
29
+ capture_io do
30
+ Time.stub(:now, TIMESTAMP) do
31
+ ActiveRecordDoctor::AddIndexesGenerator.start([path])
32
+
33
+ load(File.join("db", "migrate", "20210201131530_index_foreign_keys_in_users.rb"))
34
+ IndexForeignKeysInUsers.migrate(:up)
35
+
36
+ load(File.join("db", "migrate", "20210201131531_index_foreign_keys_in_organizations.rb"))
37
+ IndexForeignKeysInOrganizations.migrate(:up)
38
+
39
+ ::Object.send(:remove_const, :IndexForeignKeysInUsers)
40
+ ::Object.send(:remove_const, :IndexForeignKeysInOrganizations)
41
+ end
42
+ end
43
+
44
+ assert_indexes([
45
+ ["users", ["organization_id"]],
46
+ ["users", ["account_id"]],
47
+ ["organizations", ["owner_id"]]
48
+ ])
49
+
50
+ assert_equal(4, Dir.entries("./db/migrate").size)
51
+ end
52
+ end
53
+
54
+ def test_create_migrations_raises_when_malformed_inpout
55
+ Tempfile.create do |file|
56
+ file.write(<<~INDEXES)
57
+ add an index on users. - foreign keys are often used in database lookups and should be indexed for performance reasons
58
+ INDEXES
59
+ file.flush
60
+
61
+ assert_raises(RuntimeError) do
62
+ capture_io do
63
+ ActiveRecordDoctor::AddIndexesGenerator.start([file.path])
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def test_create_migrations_skips_blank_lines
70
+ Dir.mktmpdir do |dir|
71
+ Dir.chdir(dir)
72
+
73
+ path = File.join(dir, "indexes.txt")
74
+ File.write(path, "\n")
75
+
76
+ capture_io do
77
+ Time.stub(:now, TIMESTAMP) do
78
+ # Not raising an exception is considered success.
79
+ ActiveRecordDoctor::AddIndexesGenerator.start([path])
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def test_create_migrations_truncates_long_index_names
86
+ # Both the table and column names must be quite long. Otherwise, the
87
+ # we might reach table or column name length limits and fail to generate an
88
+ # index name that's long enough.
89
+ create_table(:organizations_migrated_from_legacy_app) do |t|
90
+ t.integer :legacy_owner_id_compatible_with_v1_to_v8
91
+ end
92
+
93
+ Dir.mktmpdir do |dir|
94
+ Dir.chdir(dir)
95
+
96
+ path = File.join(dir, "indexes.txt")
97
+ File.write(path, <<~INDEXES)
98
+ add an index on organizations_migrated_from_legacy_app.legacy_owner_id_compatible_with_v1_to_v8 - foreign keys are often used in database lookups and should be indexed for performance reasons
99
+ INDEXES
100
+
101
+ capture_io do
102
+ Time.stub(:now, TIMESTAMP) do
103
+ # If no exceptions are raised then we consider this to be a success.
104
+ ActiveRecordDoctor::AddIndexesGenerator.start([path])
105
+
106
+ load(File.join(
107
+ "db",
108
+ "migrate",
109
+ "20210201131530_index_foreign_keys_in_organizations_migrated_from_legacy_app.rb"
110
+ ))
111
+ ::IndexForeignKeysInOrganizationsMigratedFromLegacyApp.migrate(:up)
112
+
113
+ ::Object.send(:remove_const, :IndexForeignKeysInOrganizationsMigratedFromLegacyApp)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def assert_indexes(expected_indexes)
122
+ actual_indexes =
123
+ ActiveRecord::Base.connection.tables.map do |table|
124
+ ActiveRecord::Base.connection.indexes(table).map do |index|
125
+ [index.table, index.columns]
126
+ end
127
+ end.flatten(1)
128
+
129
+ assert_equal(expected_indexes.sort, actual_indexes.sort)
130
+ end
131
+ end
@@ -1,7 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModelFactory
4
- def self.cleanup
4
+ def create_table(*args, &block)
5
+ ModelFactory.create_table(*args, &block)
6
+ end
7
+
8
+ def create_model(*args, &block)
9
+ ModelFactory.create_model(*args, &block)
10
+ end
11
+
12
+ def cleanup_models
13
+ ModelFactory.cleanup_models
14
+ end
15
+
16
+ def self.cleanup_models
5
17
  delete_models
6
18
  drop_all_tables
7
19
  GC.start
@@ -10,27 +22,53 @@ module ModelFactory
10
22
  def self.drop_all_tables
11
23
  connection = ActiveRecord::Base.connection
12
24
  loop do
13
- before = connection.tables.size
25
+ before = connection.data_sources.size
14
26
  break if before.zero?
15
27
 
16
- attempt_drop_all_tables(connection)
17
- after = connection.tables.size
28
+ try_drop_all_tables_and_views(connection)
29
+ remaining_data_sources = connection.data_sources
30
+ after = remaining_data_sources.size
18
31
 
32
+ # rubocop:disable Style/Next
19
33
  if before == after
20
- raise("cannot delete temporary tables - most likely due to failing constraints")
34
+ raise(<<~ERROR)
35
+ Cannot delete temporary tables - most likely due to failing constraints. Remaining tables and views:
36
+
37
+ #{remaining_data_sources.join("\n")}
38
+ ERROR
39
+ end
40
+ # rubocop:enable Style/Next
41
+ end
42
+ end
43
+
44
+ def self.try_drop_all_tables_and_views(connection)
45
+ connection.data_sources.each do |table_name|
46
+ try_drop_table(connection, table_name) || try_drop_view(connection, table_name)
47
+ end
48
+ end
49
+
50
+ def self.try_drop_table(connection, table_name)
51
+ ActiveRecord::Migration.suppress_messages do
52
+ begin
53
+ connection.drop_table(table_name, force: :cascade)
54
+ true
55
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::StatementInvalid
56
+ # The table cannot be dropped due to foreign key constraints so
57
+ # we'll try to drop it on another attempt.
58
+ false
21
59
  end
22
60
  end
23
61
  end
24
62
 
25
- def self.attempt_drop_all_tables(connection)
26
- connection.tables.each do |table_name|
27
- ActiveRecord::Migration.suppress_messages do
28
- begin
29
- connection.drop_table(table_name, force: :cascade)
30
- rescue ActiveRecord::InvalidForeignKey, ActiveRecord::StatementInvalid
31
- # The table cannot be dropped due to foreign key constraints so
32
- # we'll try to drop it on another attempt.
33
- end
63
+ def self.try_drop_view(connection, view_name)
64
+ ActiveRecord::Migration.suppress_messages do
65
+ begin
66
+ connection.execute("DROP VIEW #{view_name}")
67
+ true
68
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::StatementInvalid
69
+ # The table cannot be dropped due to foreign key constraints so
70
+ # we'll try to drop it on another attempt.
71
+ false
34
72
  end
35
73
  end
36
74
  end
@@ -39,23 +77,35 @@ module ModelFactory
39
77
  Models.empty
40
78
  end
41
79
 
42
- def self.create_table(table_name, &block)
80
+ def self.create_table(table_name, options = {}, &block)
43
81
  table_name = table_name.to_sym
44
82
  ActiveRecord::Migration.suppress_messages do
45
- ActiveRecord::Migration.create_table(table_name, &block)
83
+ ActiveRecord::Migration.create_table(table_name, **options, &block)
46
84
  end
47
-
48
85
  # Return a proxy object allowing the caller to chain #create_model
49
86
  # right after creating a table so that it can be followed by the model
50
87
  # definition.
51
88
  ModelDefinitionProxy.new(table_name)
52
89
  end
53
90
 
54
- def self.create_model(table_name, &block)
55
- table_name = table_name.to_sym
56
- klass = Class.new(ActiveRecord::Base, &block)
57
- klass_name = table_name.to_s.classify
58
- Models.const_set(klass_name, klass)
91
+ def self.create_model(model_name, base_class = ActiveRecord::Base, &block)
92
+ model_name = model_name.to_sym
93
+
94
+ # Normally, when a class is defined via `class MyClass < MySuperclass` the
95
+ # .name class method returns the name of the class when called from within
96
+ # the class body. However, anonymous classes defined via Class.new DO NOT
97
+ # HAVE NAMES. They're assigned names when they're assigned to a constant.
98
+ # If we evaluated the class body, passed via block here, in the class
99
+ # definition below then some macros would break
100
+ # (e.g. has_and_belongs_to_many) due to nil name.
101
+ #
102
+ # We solve the problem by defining an empty model class first, assigning to
103
+ # a constant to ensure a name is assigned, and then reopening the class to
104
+ # give it a non-trivial body.
105
+ klass = Class.new(base_class)
106
+ Models.const_set(model_name, klass)
107
+
108
+ klass.class_eval(&block) if block_given?
59
109
  end
60
110
 
61
111
  class ModelDefinitionProxy
@@ -64,7 +114,7 @@ module ModelFactory
64
114
  end
65
115
 
66
116
  def create_model(&block)
67
- ModelFactory.create_model(@table_name, &block)
117
+ ModelFactory.create_model(@table_name.to_s.classify, &block)
68
118
  end
69
119
  end
70
120
 
data/test/setup.rb CHANGED
@@ -7,45 +7,25 @@
7
7
  require "uri"
8
8
 
9
9
  require "active_record"
10
+ require "pg"
11
+ require "mysql2"
10
12
 
11
- # Connect to the database defined in the URL.
12
- case ENV["DATABASE"]
13
- when "postgresql"
14
- require "pg"
15
- DEFAULT_DATABASE_URL = "postgres:///active_record_doctor_test"
16
- when "mysql"
17
- require "mysql2"
18
- DEFAULT_DATABASE_URL = "mysql2:///active_record_doctor_test"
19
- when nil
20
- # rubocop:disable Style/StderrPuts
21
- $stderr.puts(<<ERROR)
22
- The DATABASE environment variable is not set. It must be set before running the
23
- test suite. Valid values are "mysql" and "postgresql".
24
- ERROR
25
- # rubocop:enable Style/StderrPuts
26
- exit(1)
27
- else raise("unrecognized database #{ENV['DATABASE']}")
28
- end
29
- ActiveRecord::Base.establish_connection(ENV.fetch("DATABASE_URL", DEFAULT_DATABASE_URL))
13
+ adapter = ENV.fetch("DATABASE_ADAPTER")
14
+ ActiveRecord::Base.establish_connection(
15
+ adapter: adapter,
16
+ host: ENV["DATABASE_HOST"],
17
+ port: ENV["DATABASE_PORT"],
18
+ username: ENV["DATABASE_USERNAME"],
19
+ password: ENV["DATABASE_PASSWORD"],
20
+ database: "active_record_doctor_test"
21
+ )
22
+
23
+ puts "Using #{adapter}"
30
24
 
31
25
  # We need to call #connection to enfore Active Record to actually establish
32
26
  # the connection.
33
27
  ActiveRecord::Base.connection
34
28
 
35
- # We need to mock Rails because some detectors depend on .eager_load! This must
36
- # happen AFTER loading active_record_doctor as otherwise it'd attempt to
37
- # install a Railtie.
38
- module Rails
39
- class TestApplication
40
- def eager_load!
41
- end
42
- end
43
-
44
- def self.application
45
- @application ||= TestApplication.new
46
- end
47
- end
48
-
49
29
  # Load Active Record Doctor.
50
30
  require "active_record_doctor"
51
31
 
@@ -53,23 +33,52 @@ require "active_record_doctor"
53
33
  require "minitest"
54
34
  require "minitest/autorun"
55
35
  require "minitest/fork_executor"
56
-
57
36
  require_relative "model_factory"
58
37
 
38
+ # Filter out Minitest backtrace while allowing backtrace from other libraries
39
+ # to be shown.
40
+ Minitest.backtrace_filter = Minitest::BacktraceFilter.new
41
+
42
+ # Uncomment in case there's test case interference.
43
+ Minitest.parallel_executor = Minitest::ForkExecutor.new
44
+
59
45
  # Prepare the test class.
60
46
  class Minitest::Test
47
+ include ModelFactory
48
+
61
49
  def setup
62
- # Ensure all remnants of previous test runs, most likely in form of tables,
63
- # are removed.
64
- ModelFactory.cleanup
50
+ # Delete remnants (models and tables) of previous test case runs.
51
+ cleanup_models
65
52
  end
66
53
 
67
54
  def teardown
68
- ModelFactory.cleanup
55
+ @config_path = nil
56
+
57
+ if @previous_dir
58
+ Dir.chdir(@previous_dir)
59
+ @previous_dir = nil
60
+ end
61
+
62
+ # Ensure all remnants of previous test runs, most likely in form of tables,
63
+ # are removed.
64
+ cleanup_models
69
65
  end
70
66
 
71
67
  private
72
68
 
69
+ attr_reader :config_path
70
+
71
+ def config_file(content)
72
+ @previous_dir = Dir.pwd
73
+
74
+ directory = Dir.mktmpdir("active_record_doctor")
75
+ @config_path = File.join(directory, ".active_record_doctor")
76
+ File.write(@config_path, content)
77
+ Dir.chdir(directory)
78
+
79
+ @config_path
80
+ end
81
+
73
82
  def postgresql?
74
83
  ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
75
84
  end
@@ -78,49 +87,30 @@ class Minitest::Test
78
87
  ActiveRecord::Base.connection.adapter_name == "Mysql2"
79
88
  end
80
89
 
81
- def create_table(*args, &block)
82
- ModelFactory.create_table(*args, &block)
90
+ def detector_name
91
+ self.class.name.sub(/Test$/, "").demodulize.underscore.to_sym
83
92
  end
84
93
 
85
- def create_model(*args, &block)
86
- ModelFactory.create_model(*args, &block)
87
- end
88
-
89
- # Return the detector class under test.
90
- def detector_class
91
- self.class.name.sub(/Test$/, "").constantize
92
- end
93
-
94
- # Run the appropriate detector. The detector name is inferred from the test class.
95
94
  def run_detector
96
- detector_class.run.first
95
+ io = StringIO.new
96
+ runner = ActiveRecordDoctor::Runner.new(load_config, io)
97
+ success = runner.run_one(detector_name)
98
+ [success, io.string]
97
99
  end
98
100
 
99
- def run_task
100
- output = StringIO.new
101
- printer = ActiveRecordDoctor::Printers::IOPrinter.new(output)
102
- success = ActiveRecordDoctor::Task.new(detector_class, printer).run
103
- [success, output.string]
101
+ def load_config
102
+ ActiveRecordDoctor.load_config_with_defaults(@config_path)
104
103
  end
105
104
 
106
105
  def assert_problems(expected_output)
107
- success, actual_output = run_task
108
-
109
- assert_equal(expected_output, actual_output)
110
- refute(success)
106
+ success, output = run_detector
107
+ assert_equal(expected_output, output)
108
+ refute(success, "Expected the detector to return failure.")
111
109
  end
112
110
 
113
- def refute_problems
114
- success, actual_output = run_task
115
-
116
- assert_equal("", actual_output)
117
- assert(success)
111
+ def refute_problems(expected_output = "")
112
+ success, output = run_detector
113
+ assert_equal(expected_output, output)
114
+ assert(success, "Expected the detector to return success.")
118
115
  end
119
116
  end
120
-
121
- # Filter out Minitest backtrace while allowing backtrace from other libraries
122
- # to be shown.
123
- Minitest.backtrace_filter = Minitest::BacktraceFilter.new
124
-
125
- # Uncomment in case there's test case interference.
126
- Minitest.parallel_executor = Minitest::ForkExecutor.new