with_model 2.3.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +0 -12
- data/CHANGELOG.md +17 -0
- data/CONTRIBUTING.md +42 -7
- data/Gemfile +1 -0
- data/README.md +108 -12
- data/Rakefile +7 -1
- data/docs/minitest.md +45 -0
- data/docs/rspec.md +43 -0
- data/docs/test-unit.md +73 -0
- data/lib/with_model/descendants_tracker.rb +3 -11
- data/lib/with_model/minitest.rb +7 -0
- data/lib/with_model/model/dsl.rb +3 -1
- data/lib/with_model/model.rb +14 -12
- data/lib/with_model/null_table.rb +49 -19
- data/lib/with_model/rspec.rb +10 -0
- data/lib/with_model/table.rb +8 -8
- data/lib/with_model/test_unit.rb +7 -0
- data/lib/with_model/version.rb +1 -1
- data/lib/with_model.rb +6 -1
- data/spec/descendants_tracking_spec.rb +20 -2
- data/spec/readme_spec.rb +20 -0
- data/spec/runner_entrypoints_spec.rb +153 -0
- data/spec/table_false_spec.rb +169 -4
- data/spec/table_spec.rb +37 -0
- data/test/own_table_name_test.rb +81 -0
- data/test_unit/lifecycle_test.rb +186 -0
- data/test_unit/own_table_name_test.rb +81 -0
- data/test_unit/sti_test.rb +71 -0
- data/test_unit/test_unit_helper.rb +15 -0
- data/test_unit/with_model_test.rb +58 -0
- data/with_model.gemspec +3 -2
- metadata +22 -6
|
@@ -4,9 +4,11 @@ require "active_record"
|
|
|
4
4
|
require "with_model/invalid_superclass"
|
|
5
5
|
|
|
6
6
|
module WithModel
|
|
7
|
-
# Stands in for a {WithModel::Table} when a model should
|
|
8
|
-
#
|
|
9
|
-
# Inheritance requires
|
|
7
|
+
# Stands in for a {WithModel::Table} when a model should not get a table of its
|
|
8
|
+
# own: either it inherits its superclass's table, as Rails Single Table
|
|
9
|
+
# Inheritance requires, or it uses an existing table: one its model block
|
|
10
|
+
# names with `self.table_name`, or the one Active Record names after the
|
|
11
|
+
# model. An abstract model needs neither. Selected by `table(false)`.
|
|
10
12
|
#
|
|
11
13
|
# In general, direct use of this class should be avoided. Instead use
|
|
12
14
|
# either the {WithModel high-level API} or {WithModel::Model::DSL low-level API}.
|
|
@@ -21,7 +23,9 @@ module WithModel
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# Creates nothing, but raises {WithModel::InvalidSuperclass} when the
|
|
24
|
-
# superclass cannot support single table inheritance.
|
|
26
|
+
# superclass has a table that cannot support single table inheritance.
|
|
27
|
+
# Whether a superclass with nothing to inherit is a problem depends on the
|
|
28
|
+
# model block, so that waits for {#verify}.
|
|
25
29
|
#
|
|
26
30
|
# Errors describe the model's situation rather than the call that produced
|
|
27
31
|
# it. Any expression that evaluates to false selects a NullTable, so there
|
|
@@ -34,14 +38,13 @@ module WithModel
|
|
|
34
38
|
# `StatementInvalid` naming the table if it never appears, so raising here
|
|
35
39
|
# would only forbid working setups.
|
|
36
40
|
#
|
|
37
|
-
# What is left is {WithModel::InvalidSuperclass}:
|
|
38
|
-
#
|
|
39
|
-
#
|
|
41
|
+
# What is left is {WithModel::InvalidSuperclass}: a missing inheritance
|
|
42
|
+
# column, like a missing table to inherit, is a permanent fact about the
|
|
43
|
+
# superclass passed in, not about when it was looked at, so no amount of
|
|
44
|
+
# waiting makes it work.
|
|
40
45
|
def create
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Nothing more can be checked until there is a table to look at.
|
|
44
|
-
return unless table_exists?
|
|
46
|
+
# Nothing can be checked until there is a table to look at.
|
|
47
|
+
return unless inheritable_table? && table_exists?
|
|
45
48
|
return if inheritance_column?
|
|
46
49
|
|
|
47
50
|
raise_invalid_superclass "#{@superclass}'s table #{@superclass.table_name.inspect} has no " \
|
|
@@ -54,9 +57,33 @@ module WithModel
|
|
|
54
57
|
def configure(klass)
|
|
55
58
|
end
|
|
56
59
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
+
# Raises {WithModel::InvalidSuperclass} when the superclass has no table to
|
|
61
|
+
# inherit (`ActiveRecord::Base`, or an abstract class) and the model's own
|
|
62
|
+
# table - the one its block named, or else the one Active Record names after
|
|
63
|
+
# the model - does not exist once its block has run. A model its block made
|
|
64
|
+
# abstract needs no table, so it always passes.
|
|
65
|
+
#
|
|
66
|
+
# Unlike a superclass's missing table, which {#create} lets through, this
|
|
67
|
+
# table must already exist. A model with nothing to inherit is far more
|
|
68
|
+
# likely a mistaken attempt at single table inheritance than a deliberate
|
|
69
|
+
# reference to a table that is yet to be created, and with nothing to point
|
|
70
|
+
# at, Active Record's own error would leave that mistake unexplained.
|
|
71
|
+
def verify(klass)
|
|
72
|
+
return if klass.abstract_class? || inheritable_table? || own_table_exists?(klass)
|
|
73
|
+
|
|
74
|
+
raise_invalid_superclass "#{@superclass} has none to inherit and there is no " \
|
|
75
|
+
"#{klass.table_name.inspect} table. To use an existing table, create it before " \
|
|
76
|
+
"the model or set `self.table_name` in its model block."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Removes the rows a single table inheritance subclass wrote, which would
|
|
80
|
+
# otherwise outlive the constant that names them and make the superclass
|
|
81
|
+
# unloadable (`ActiveRecord::SubclassNotFound`).
|
|
82
|
+
#
|
|
83
|
+
# A model that is its own base class names a table that belongs to the
|
|
84
|
+
# suite, and its rows name no class, so they are left for the suite to clean
|
|
85
|
+
# up like the rest of that table - its table need not have an inheritance
|
|
86
|
+
# column to delete by, either.
|
|
60
87
|
#
|
|
61
88
|
# `unscoped` is required because a `default_scope` on the superclass would
|
|
62
89
|
# otherwise hide rows from cleanup; the inheritance-column condition is
|
|
@@ -66,7 +93,7 @@ module WithModel
|
|
|
66
93
|
# A table that does not exist holds no rows to remove, and failing here
|
|
67
94
|
# would fail an example whose body had already passed.
|
|
68
95
|
def teardown(klass)
|
|
69
|
-
return
|
|
96
|
+
return if klass.base_class? || !klass.table_exists?
|
|
70
97
|
|
|
71
98
|
klass.unscoped.where(klass.inheritance_column => klass.sti_name).each(&:destroy!)
|
|
72
99
|
end
|
|
@@ -79,10 +106,13 @@ module WithModel
|
|
|
79
106
|
|
|
80
107
|
def inheritance_column = @superclass.inheritance_column
|
|
81
108
|
|
|
82
|
-
# `ActiveRecord::Base` and abstract
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
109
|
+
# `ActiveRecord::Base` has no `table_name`, and an abstract class has none to
|
|
110
|
+
# pass on through single table inheritance even when it has one: Active
|
|
111
|
+
# Record makes each of its subclasses a base class of its own. Neither test
|
|
112
|
+
# covers both, since `abstract_class?` is false for `ActiveRecord::Base`.
|
|
113
|
+
def inheritable_table? = !@superclass.abstract_class? && @superclass.table_name.present?
|
|
114
|
+
|
|
115
|
+
def own_table_exists?(klass) = klass.connection.data_source_exists?(klass.table_name)
|
|
86
116
|
|
|
87
117
|
def table_exists? = @superclass.table_exists?
|
|
88
118
|
|
data/lib/with_model/table.rb
CHANGED
|
@@ -30,6 +30,12 @@ module WithModel
|
|
|
30
30
|
klass.table_name = @name
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Accepts any model block: the table is the model's own. Present so that
|
|
34
|
+
# {WithModel::NullTable} - which checks the model once its block has run -
|
|
35
|
+
# can stand in here.
|
|
36
|
+
def verify(_klass)
|
|
37
|
+
end
|
|
38
|
+
|
|
33
39
|
# Removes everything this table holds by dropping it. The model is not
|
|
34
40
|
# needed, but is accepted so that {WithModel::NullTable} - which does need
|
|
35
41
|
# it - can stand in here.
|
|
@@ -38,19 +44,13 @@ module WithModel
|
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def destroy
|
|
41
|
-
connection.drop_table(@name)
|
|
47
|
+
connection.drop_table(@name) if exists?
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
private
|
|
45
51
|
|
|
46
52
|
attr_reader :connection
|
|
47
53
|
|
|
48
|
-
def exists?
|
|
49
|
-
if connection.respond_to?(:data_source_exists?)
|
|
50
|
-
connection.data_source_exists?(@name)
|
|
51
|
-
else
|
|
52
|
-
connection.table_exists?(@name)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
54
|
+
def exists? = connection.data_source_exists?(@name)
|
|
55
55
|
end
|
|
56
56
|
end
|
data/lib/with_model/version.rb
CHANGED
data/lib/with_model.rb
CHANGED
|
@@ -113,8 +113,13 @@ module WithModel
|
|
|
113
113
|
class_eval do
|
|
114
114
|
include MiniTestLifeCycle.call(object)
|
|
115
115
|
end
|
|
116
|
+
when :test_unit
|
|
117
|
+
# These options place creation before and destruction after user callbacks,
|
|
118
|
+
# preserving declaration and reverse order across inheritance.
|
|
119
|
+
setup(before: :append) { object.create }
|
|
120
|
+
teardown(after: :prepend) { object.destroy }
|
|
116
121
|
else
|
|
117
|
-
raise ArgumentError, "Unsupported test runner set, expected :rspec or :
|
|
122
|
+
raise ArgumentError, "Unsupported test runner set, expected :rspec, :minitest, or :test_unit"
|
|
118
123
|
end
|
|
119
124
|
end
|
|
120
125
|
end
|
|
@@ -30,7 +30,6 @@ describe "Descendants tracking" do
|
|
|
30
30
|
|
|
31
31
|
context "with ActiveSupport::DescendantsTracker (cache_classes: true)" do
|
|
32
32
|
before do
|
|
33
|
-
expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_falsey
|
|
34
33
|
expect { ActiveSupport::DescendantsTracker.clear([]) }.not_to raise_exception
|
|
35
34
|
end
|
|
36
35
|
|
|
@@ -40,10 +39,29 @@ describe "Descendants tracking" do
|
|
|
40
39
|
context "without ActiveSupport::DescendantsTracker (cache_classes: false)" do
|
|
41
40
|
before do
|
|
42
41
|
ActiveSupport::DescendantsTracker.disable_clear!
|
|
43
|
-
expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_truthy
|
|
44
42
|
expect { ActiveSupport::DescendantsTracker.clear([]) }.to raise_exception(RuntimeError)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
include_examples "clearing descendants between test runs"
|
|
48
46
|
end
|
|
47
|
+
|
|
48
|
+
it "keeps filtering ahead of later class method extensions" do
|
|
49
|
+
raw_subclasses = Class.instance_method(:subclasses).super_method
|
|
50
|
+
raw_descendants = Class.instance_method(:descendants).super_method
|
|
51
|
+
override = Module.new do
|
|
52
|
+
define_method(:subclasses) { raw_subclasses.bind_call(self) }
|
|
53
|
+
define_method(:descendants) { raw_descendants.bind_call(self) }
|
|
54
|
+
end
|
|
55
|
+
destroyed_model = stub_const("DestroyedModel", Class.new(ActiveRecord::Base))
|
|
56
|
+
WithModel::DescendantsTracker.clear([destroyed_model])
|
|
57
|
+
|
|
58
|
+
ActiveRecord::Base.extend override
|
|
59
|
+
|
|
60
|
+
expect(ActiveRecord::Base.descendants).not_to include(destroyed_model)
|
|
61
|
+
ensure
|
|
62
|
+
override&.module_eval do
|
|
63
|
+
remove_method :subclasses
|
|
64
|
+
remove_method :descendants
|
|
65
|
+
end
|
|
66
|
+
end
|
|
49
67
|
end
|
data/spec/readme_spec.rb
CHANGED
|
@@ -145,6 +145,26 @@ describe "with_model supports Single Table Inheritance" do
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
describe "with_model supports existing tables" do
|
|
149
|
+
with_table :legendary_creatures do |t|
|
|
150
|
+
t.string "name"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
with_model :Phoenix do
|
|
154
|
+
table(false)
|
|
155
|
+
|
|
156
|
+
model do
|
|
157
|
+
self.table_name = "legendary_creatures"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "uses the table it names" do
|
|
162
|
+
Phoenix.create!(name: "Fawkes")
|
|
163
|
+
|
|
164
|
+
expect(Phoenix.table_name).to eq "legendary_creatures"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
148
168
|
describe "with_model supports foreign keys" do
|
|
149
169
|
with_model :Author do
|
|
150
170
|
table
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
RSpec.describe "runner integration entrypoints" do
|
|
8
|
+
def run_ruby(source, load_paths: ["lib"])
|
|
9
|
+
Open3.capture3(
|
|
10
|
+
RbConfig.ruby,
|
|
11
|
+
*load_paths.map { |path| "-I#{path}" },
|
|
12
|
+
"-e",
|
|
13
|
+
source
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def expect_native_runner_to_pass(source, load_paths: ["lib"])
|
|
18
|
+
stdout, stderr, status = run_ruby(source, load_paths:)
|
|
19
|
+
|
|
20
|
+
expect(status).to be_success, <<~MESSAGE
|
|
21
|
+
subprocess failed with status #{status.exitstatus}
|
|
22
|
+
stdout:
|
|
23
|
+
#{stdout}
|
|
24
|
+
stderr:
|
|
25
|
+
#{stderr}
|
|
26
|
+
MESSAGE
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "configures RSpec and extends new example groups" do
|
|
30
|
+
expect_native_runner_to_pass(<<~'RUBY')
|
|
31
|
+
require "with_model/rspec"
|
|
32
|
+
abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :rspec
|
|
33
|
+
|
|
34
|
+
RSpec.describe "entrypoint" do
|
|
35
|
+
it "installs the DSL" do
|
|
36
|
+
expect(self.class).to respond_to(:with_model)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
exit RSpec::Core::Runner.run([])
|
|
41
|
+
RUBY
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "configures Minitest and extends new test subclasses" do
|
|
45
|
+
expect_native_runner_to_pass(<<~'RUBY')
|
|
46
|
+
require "with_model/minitest"
|
|
47
|
+
abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :minitest
|
|
48
|
+
|
|
49
|
+
# Assert that minitest/autorun is not loaded
|
|
50
|
+
autorun_loaded = $LOADED_FEATURES.any? { |f| f.end_with?("/minitest/autorun.rb") }
|
|
51
|
+
abort "autorun should not be loaded; loaded features: #{$LOADED_FEATURES.inspect}" if autorun_loaded
|
|
52
|
+
|
|
53
|
+
class MinitestEntrypointTest < Minitest::Test
|
|
54
|
+
def test_installs_the_dsl
|
|
55
|
+
assert_respond_to self.class, :with_model
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
exit(Minitest.run ? 0 : 1)
|
|
60
|
+
RUBY
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "configures Test::Unit and extends new test subclasses" do
|
|
64
|
+
expect_native_runner_to_pass(<<~'RUBY')
|
|
65
|
+
require "with_model/test_unit"
|
|
66
|
+
abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :test_unit
|
|
67
|
+
|
|
68
|
+
class TestUnitEntrypointTest < Test::Unit::TestCase
|
|
69
|
+
def test_installs_the_dsl
|
|
70
|
+
assert_respond_to self.class, :with_model
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
RUBY
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "propagates Test::Unit's LoadError when the framework is unavailable" do
|
|
77
|
+
Dir.mktmpdir do |directory|
|
|
78
|
+
Dir.mkdir(File.join(directory, "test"))
|
|
79
|
+
File.write(
|
|
80
|
+
File.join(directory, "test/unit.rb"),
|
|
81
|
+
'raise LoadError, "distinctive test/unit LoadError"'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect_native_runner_to_pass(<<~'RUBY', load_paths: [directory, "lib"])
|
|
85
|
+
begin
|
|
86
|
+
require "with_model/test_unit"
|
|
87
|
+
rescue LoadError => error
|
|
88
|
+
abort "unexpected LoadError: #{error.message.inspect}" unless
|
|
89
|
+
error.message == "distinctive test/unit LoadError"
|
|
90
|
+
else
|
|
91
|
+
abort "expected a LoadError"
|
|
92
|
+
end
|
|
93
|
+
RUBY
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "retains manual Test::Unit setup with plain with_model" do
|
|
98
|
+
expect_native_runner_to_pass(<<~'RUBY')
|
|
99
|
+
require "test/unit"
|
|
100
|
+
require "with_model"
|
|
101
|
+
|
|
102
|
+
WithModel.runner = :test_unit
|
|
103
|
+
Test::Unit::TestCase.extend WithModel
|
|
104
|
+
abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :test_unit
|
|
105
|
+
|
|
106
|
+
class PlainWithModelTestUnitTest < Test::Unit::TestCase
|
|
107
|
+
def test_installs_the_dsl
|
|
108
|
+
assert_respond_to self.class, :with_model
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
RUBY
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "retains manual RSpec setup with plain with_model" do
|
|
115
|
+
expect_native_runner_to_pass(<<~'RUBY')
|
|
116
|
+
require "rspec"
|
|
117
|
+
require "with_model"
|
|
118
|
+
|
|
119
|
+
WithModel.runner = :rspec
|
|
120
|
+
RSpec.configure do |config|
|
|
121
|
+
config.extend WithModel
|
|
122
|
+
end
|
|
123
|
+
abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :rspec
|
|
124
|
+
|
|
125
|
+
RSpec.describe "entrypoint" do
|
|
126
|
+
it "installs the DSL" do
|
|
127
|
+
expect(self.class).to respond_to(:with_model)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
exit RSpec::Core::Runner.run([])
|
|
132
|
+
RUBY
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "retains manual Minitest setup with plain with_model" do
|
|
136
|
+
expect_native_runner_to_pass(<<~'RUBY')
|
|
137
|
+
require "minitest"
|
|
138
|
+
require "with_model"
|
|
139
|
+
|
|
140
|
+
WithModel.runner = :minitest
|
|
141
|
+
Minitest::Test.extend WithModel
|
|
142
|
+
abort "runner: #{WithModel.runner.inspect}" unless WithModel.runner == :minitest
|
|
143
|
+
|
|
144
|
+
class PlainWithModelMinitestTest < Minitest::Test
|
|
145
|
+
def test_installs_the_dsl
|
|
146
|
+
assert_respond_to self.class, :with_model
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
exit(Minitest.run ? 0 : 1)
|
|
151
|
+
RUBY
|
|
152
|
+
end
|
|
153
|
+
end
|
data/spec/table_false_spec.rb
CHANGED
|
@@ -21,10 +21,6 @@ RSpec.describe "table(false)" do
|
|
|
21
21
|
classes = @stubbed_active_record_classes
|
|
22
22
|
next unless classes
|
|
23
23
|
|
|
24
|
-
ActiveSupport::DescendantsTracker.clear(classes) \
|
|
25
|
-
unless ActiveSupport::DescendantsTracker.clear_disabled
|
|
26
|
-
ActiveSupport::Dependencies::Reference.clear! \
|
|
27
|
-
if defined?(ActiveSupport::Dependencies::Reference)
|
|
28
24
|
WithModel::DescendantsTracker.clear(classes)
|
|
29
25
|
end
|
|
30
26
|
|
|
@@ -437,6 +433,135 @@ RSpec.describe "table(false)" do
|
|
|
437
433
|
end
|
|
438
434
|
end
|
|
439
435
|
|
|
436
|
+
# Not single table inheritance at all: the model is its own base class and
|
|
437
|
+
# names an existing table, such as a legacy table or one several models share.
|
|
438
|
+
describe "with a model that sets its own table_name" do
|
|
439
|
+
with_table :legendary_creatures do |t|
|
|
440
|
+
t.string :name
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Declared between the two so that it runs after the model is torn down but
|
|
444
|
+
# before the table is dropped.
|
|
445
|
+
after do
|
|
446
|
+
expect(ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM legendary_creatures"))
|
|
447
|
+
.to eq @rows_written.to_i
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
with_model :Phoenix do
|
|
451
|
+
table(false)
|
|
452
|
+
model do
|
|
453
|
+
self.table_name = "legendary_creatures"
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "uses the table it names" do
|
|
458
|
+
Phoenix.create!(name: "Fawkes")
|
|
459
|
+
@rows_written = 1
|
|
460
|
+
|
|
461
|
+
expect(Phoenix.table_name).to eq "legendary_creatures"
|
|
462
|
+
expect(Phoenix.count).to eq 1
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
it "creates no table of its own" do
|
|
466
|
+
expect(ActiveRecord::Base.connection.tables.grep(/with_model_phoenix/)).to be_empty
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it "is its own base class" do
|
|
470
|
+
expect(Phoenix.base_class).to eq Phoenix
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# The table belongs to the suite, which is left to clean it up - with
|
|
474
|
+
# transactional tests, or however it cleans up its other tables. Deleting by
|
|
475
|
+
# type would fail outright here, since there is no type column.
|
|
476
|
+
it "leaves its rows alone when torn down" do
|
|
477
|
+
Phoenix.create!(name: "Fawkes")
|
|
478
|
+
Phoenix.create!(name: "Buckbeak")
|
|
479
|
+
@rows_written = 2
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# An abstract model needs no table, so `table(false)` suits it too.
|
|
483
|
+
context "with an abstract superclass that has no table" do
|
|
484
|
+
with_model :ApplicationRecord do
|
|
485
|
+
table(false)
|
|
486
|
+
model do
|
|
487
|
+
self.abstract_class = true
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
with_model :Griffin, superclass: :ApplicationRecord do
|
|
492
|
+
table(false)
|
|
493
|
+
model do
|
|
494
|
+
self.table_name = "legendary_creatures"
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
it "uses the table it names" do
|
|
499
|
+
Griffin.create!(name: "Buckbeak")
|
|
500
|
+
@rows_written = 1
|
|
501
|
+
|
|
502
|
+
expect(Griffin.table_name).to eq "legendary_creatures"
|
|
503
|
+
expect(Griffin.count).to eq 1
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it "leaves the abstract superclass without a table" do
|
|
507
|
+
expect(ApplicationRecord.table_name).to be_nil
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Active Record never applies single table inheritance below an abstract
|
|
512
|
+
# class, so its table needs no inheritance column.
|
|
513
|
+
context "with an abstract superclass that has a table" do
|
|
514
|
+
with_model :LegendaryCreature do
|
|
515
|
+
table
|
|
516
|
+
model do
|
|
517
|
+
self.abstract_class = true
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
with_model :Sphinx, superclass: :LegendaryCreature do
|
|
522
|
+
table(false)
|
|
523
|
+
model do
|
|
524
|
+
self.table_name = "legendary_creatures"
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
it "uses the table it names" do
|
|
529
|
+
Sphinx.create!(name: "Phix")
|
|
530
|
+
@rows_written = 1
|
|
531
|
+
|
|
532
|
+
expect(Sphinx.table_name).to eq "legendary_creatures"
|
|
533
|
+
expect(Sphinx.count).to eq 1
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
it "is its own base class" do
|
|
537
|
+
expect(Sphinx.base_class).to eq Sphinx
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Active Record's naming convention picks the table, so the model block need
|
|
543
|
+
# not name it.
|
|
544
|
+
describe "with a model named after an existing table" do
|
|
545
|
+
with_table :dragons do |t|
|
|
546
|
+
t.string :name
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
with_model :Dragon do
|
|
550
|
+
table(false)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
it "uses that table" do
|
|
554
|
+
Dragon.create!(name: "Norbert")
|
|
555
|
+
|
|
556
|
+
expect(Dragon.table_name).to eq "dragons"
|
|
557
|
+
expect(Dragon.count).to eq 1
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
it "creates no table of its own" do
|
|
561
|
+
expect(ActiveRecord::Base.connection.tables.grep(/with_model_dragon/)).to be_empty
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
440
565
|
describe "invalid configurations" do
|
|
441
566
|
# Creates immediately the same tableless subclass normally generated by:
|
|
442
567
|
#
|
|
@@ -465,6 +590,46 @@ RSpec.describe "table(false)" do
|
|
|
465
590
|
.to raise_error(WithModel::InvalidSuperclass, /ActiveRecord::Base has none to inherit/)
|
|
466
591
|
end
|
|
467
592
|
|
|
593
|
+
it "names the table it looked for" do
|
|
594
|
+
expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
|
|
595
|
+
.to raise_error(WithModel::InvalidSuperclass, /there is no "tableless_subclasses" table/)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
it "suggests creating that table first or naming another" do
|
|
599
|
+
expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
|
|
600
|
+
.to raise_error(
|
|
601
|
+
WithModel::InvalidSuperclass,
|
|
602
|
+
/create it before the model or set `self.table_name` in its model block/
|
|
603
|
+
)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Builds the model by hand so that, should the check let it through, the
|
|
607
|
+
# example can still destroy it rather than leak its constant.
|
|
608
|
+
it "looks for the table the model block names" do
|
|
609
|
+
subclass = WithModel::Model.new(:TablelessSubclass, superclass: ActiveRecord::Base)
|
|
610
|
+
WithModel::Model::DSL.new(subclass).instance_eval do
|
|
611
|
+
table(false)
|
|
612
|
+
model { self.table_name = "missing_furniture" }
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
begin
|
|
616
|
+
expect { subclass.create }
|
|
617
|
+
.to raise_error(WithModel::InvalidSuperclass, /there is no "missing_furniture" table/)
|
|
618
|
+
ensure
|
|
619
|
+
subclass.destroy if Object.const_defined?(:TablelessSubclass, false)
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# The check needs the model block to have run, so the constant already
|
|
624
|
+
# exists when it fails. A test runner tears down after a failed setup, but
|
|
625
|
+
# nothing else does, and a leftover constant would leak into later tests.
|
|
626
|
+
it "leaves no constant behind" do
|
|
627
|
+
expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
|
|
628
|
+
.to raise_error(WithModel::InvalidSuperclass)
|
|
629
|
+
|
|
630
|
+
expect(Object.const_defined?(:TablelessSubclass, false)).to be false
|
|
631
|
+
end
|
|
632
|
+
|
|
468
633
|
context "with an abstract superclass" do
|
|
469
634
|
with_model :TablelessSuperclass do
|
|
470
635
|
table
|
data/spec/table_spec.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe WithModel::Table do
|
|
6
|
+
let(:connection_owner) do
|
|
7
|
+
klass = Class.new(ActiveRecord::Base)
|
|
8
|
+
stub_const("TableSpecConnection", klass)
|
|
9
|
+
klass.abstract_class = true
|
|
10
|
+
klass.establish_connection(adapter: "sqlite3", database: ":memory:")
|
|
11
|
+
klass
|
|
12
|
+
end
|
|
13
|
+
let(:connection) { connection_owner.connection }
|
|
14
|
+
let(:table) { described_class.new(:temporary_records, connection:) }
|
|
15
|
+
|
|
16
|
+
after { connection_owner.connection_pool.disconnect! }
|
|
17
|
+
|
|
18
|
+
describe "#destroy" do
|
|
19
|
+
it "does not raise after its connection reconnects before teardown" do
|
|
20
|
+
table.create
|
|
21
|
+
|
|
22
|
+
connection.disconnect!
|
|
23
|
+
connection.reconnect!
|
|
24
|
+
|
|
25
|
+
expect(connection.data_source_exists?(:temporary_records)).to be false
|
|
26
|
+
expect { table.destroy }.not_to raise_error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "drops an existing table" do
|
|
30
|
+
table.create
|
|
31
|
+
|
|
32
|
+
table.destroy
|
|
33
|
+
|
|
34
|
+
expect(connection.data_source_exists?(:temporary_records)).to be false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|