with_model 2.3.0 → 2.4.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 +20 -3
- data/Gemfile +1 -0
- data/README.md +47 -10
- 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.rb +0 -13
- data/lib/with_model/null_table.rb +11 -11
- data/lib/with_model/rspec.rb +10 -0
- data/lib/with_model/table.rb +2 -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/runner_entrypoints_spec.rb +153 -0
- data/spec/table_false_spec.rb +500 -348
- data/spec/table_spec.rb +37 -0
- data/spec/with_model_spec.rb +2 -2
- data/test_unit/lifecycle_test.rb +186 -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 +20 -6
data/spec/table_false_spec.rb
CHANGED
|
@@ -2,515 +2,667 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
|
|
5
|
+
RSpec.describe "table(false)" do
|
|
6
|
+
# Defines a named Active Record class for one example, equivalent to:
|
|
7
|
+
#
|
|
8
|
+
# class HasLateTable < ActiveRecord::Base
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# RSpec removes the constant afterward; the matching `after` hook below also
|
|
12
|
+
# clears Active Record's descendant trackers so the class object cannot leak.
|
|
13
|
+
def stub_active_record_class(name, superclass: ActiveRecord::Base)
|
|
14
|
+
klass = Class.new(superclass)
|
|
15
|
+
stub_const(name.to_s, klass)
|
|
16
|
+
(@stubbed_active_record_classes ||= []) << klass
|
|
17
|
+
klass
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
after do
|
|
21
|
+
classes = @stubbed_active_record_classes
|
|
22
|
+
next unless classes
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
# which one with_model actually reads.
|
|
22
|
-
class HasCustomInheritanceColumnAndTypeColumn < ActiveRecord::Base
|
|
23
|
-
self.inheritance_column = "kind"
|
|
24
|
+
WithModel::DescendantsTracker.clear(classes)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
describe "with a concrete superclass" do
|
|
28
|
+
with_model :HasTypeColumn do
|
|
29
|
+
table do |t|
|
|
30
|
+
t.string :type
|
|
31
|
+
t.string :name
|
|
32
|
+
end
|
|
33
|
+
end
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
with_model :Truck, superclass: :HasTypeColumn do
|
|
36
|
+
table(false)
|
|
37
|
+
model do
|
|
38
|
+
def honk = "beep"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
it "inherits the superclass table rather than creating its own" do
|
|
43
|
+
expect(Truck.table_name).to eq HasTypeColumn.table_name
|
|
44
|
+
end
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
it "creates no table of its own" do
|
|
47
|
+
expect(ActiveRecord::Base.connection.tables.grep(/with_model_truck/)).to be_empty
|
|
48
|
+
end
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.abstract_class = true
|
|
45
|
-
end
|
|
50
|
+
it "reports the superclass as the STI base class" do
|
|
51
|
+
expect(Truck.base_class).to eq HasTypeColumn
|
|
52
|
+
end
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
it "still evaluates the model block" do
|
|
55
|
+
expect(Truck.new.honk).to eq "beep"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "writes its own name to the inheritance column" do
|
|
59
|
+
Truck.create!(name: "big")
|
|
60
|
+
expect(HasTypeColumn.first.type).to eq "Truck"
|
|
61
|
+
end
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
it "is found through the superclass as an instance of itself" do
|
|
64
|
+
Truck.create!(name: "big")
|
|
65
|
+
expect(HasTypeColumn.first).to be_a Truck
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Deliberately a second example asserting the same thing: both models are
|
|
69
|
+
# rebuilt for every example, so a superclass resolved once and cached on the
|
|
70
|
+
# Model instance would still point to the first example's dead parent.
|
|
71
|
+
it "works in a second example too" do
|
|
72
|
+
Truck.create!(name: "also big")
|
|
73
|
+
expect(HasTypeColumn.first).to be_a Truck
|
|
74
|
+
end
|
|
53
75
|
end
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
describe "teardown" do
|
|
78
|
+
with_model :HasTypeColumn do
|
|
79
|
+
table do |t|
|
|
58
80
|
t.string :type
|
|
59
81
|
t.string :name
|
|
60
82
|
end
|
|
83
|
+
end
|
|
61
84
|
|
|
62
|
-
|
|
85
|
+
context "when child and superclass rows exist" do
|
|
86
|
+
after do
|
|
87
|
+
expect(HasTypeColumn.where(type: "Truck").count).to eq 0
|
|
88
|
+
expect(HasTypeColumn.where(type: nil).count).to eq 1
|
|
89
|
+
end
|
|
63
90
|
|
|
64
|
-
with_model :Truck, superclass: HasTypeColumn do
|
|
91
|
+
with_model :Truck, superclass: :HasTypeColumn do
|
|
65
92
|
table(false)
|
|
66
|
-
model do
|
|
67
|
-
def honk = "beep"
|
|
68
|
-
end
|
|
69
93
|
end
|
|
70
94
|
|
|
71
|
-
it "
|
|
72
|
-
|
|
95
|
+
it "deletes its own rows and leaves the superclass's rows alone" do
|
|
96
|
+
Truck.create!(name: "child row")
|
|
97
|
+
HasTypeColumn.create!(name: "parent row")
|
|
73
98
|
end
|
|
99
|
+
end
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
context "when a child row exists" do
|
|
102
|
+
after do
|
|
103
|
+
expect { HasTypeColumn.first }.not_to raise_error
|
|
77
104
|
end
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
with_model :Truck, superclass: :HasTypeColumn do
|
|
107
|
+
table(false)
|
|
81
108
|
end
|
|
82
109
|
|
|
83
|
-
it "
|
|
84
|
-
|
|
110
|
+
it "leaves the superclass queryable" do
|
|
111
|
+
Truck.create!(name: "child row")
|
|
85
112
|
end
|
|
113
|
+
end
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
context "with an abstract superclass" do
|
|
116
|
+
with_model :TablelessSuperclass do
|
|
117
|
+
table
|
|
118
|
+
model do
|
|
119
|
+
self.abstract_class = true
|
|
120
|
+
self.table_name = nil
|
|
121
|
+
end
|
|
90
122
|
end
|
|
91
123
|
|
|
92
|
-
it "
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
124
|
+
it "does not delete rows when create raises WithModel::InvalidSuperclass" do
|
|
125
|
+
HasTypeColumn.create!(name: "parent row")
|
|
126
|
+
tableless_subclass = WithModel::Model.new(
|
|
127
|
+
:TablelessSubclass,
|
|
128
|
+
superclass: TablelessSuperclass
|
|
129
|
+
)
|
|
130
|
+
WithModel::Model::DSL.new(tableless_subclass).table(false)
|
|
131
|
+
|
|
132
|
+
expect { tableless_subclass.create }.to raise_error(WithModel::InvalidSuperclass)
|
|
133
|
+
expect { tableless_subclass.destroy }.not_to raise_error
|
|
96
134
|
|
|
97
|
-
|
|
98
|
-
# resolved once and cached on the Model instance would go stale here,
|
|
99
|
-
# because the parent is the same class object but the child is rebuilt.
|
|
100
|
-
it "works in a second example too" do
|
|
101
|
-
Truck.create!(name: "also big")
|
|
102
|
-
expect(HasTypeColumn.first).to be_a Truck
|
|
135
|
+
expect(HasTypeColumn.count).to eq 1
|
|
103
136
|
end
|
|
104
137
|
end
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
t
|
|
109
|
-
|
|
139
|
+
context "when destruction of its row is aborted" do
|
|
140
|
+
with_model :AbortsDestroy do
|
|
141
|
+
table do |t|
|
|
142
|
+
t.string :type
|
|
143
|
+
end
|
|
144
|
+
model do
|
|
145
|
+
before_destroy { throw :abort }
|
|
146
|
+
end
|
|
110
147
|
end
|
|
111
148
|
|
|
112
|
-
|
|
149
|
+
subject(:null_table) do
|
|
150
|
+
WithModel::NullTable.new(AbortsDestroy, :TemporaryAbortedDestroy)
|
|
151
|
+
end
|
|
113
152
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
WithModel::Model::DSL.new(model).instance_eval(&dsl)
|
|
117
|
-
end
|
|
153
|
+
after do
|
|
154
|
+
expect(Object.const_defined?(:TemporaryAbortedDestroy, false)).to be false
|
|
118
155
|
end
|
|
119
156
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Truck.create!(name: "child row")
|
|
124
|
-
HasTypeColumn.create!(name: "parent row")
|
|
157
|
+
with_model :TemporaryAbortedDestroy, superclass: :AbortsDestroy do
|
|
158
|
+
table(false)
|
|
159
|
+
end
|
|
125
160
|
|
|
126
|
-
|
|
161
|
+
it "fails loudly" do
|
|
162
|
+
TemporaryAbortedDestroy.create!
|
|
127
163
|
|
|
128
|
-
expect
|
|
129
|
-
|
|
164
|
+
expect do
|
|
165
|
+
null_table.teardown(TemporaryAbortedDestroy)
|
|
166
|
+
end.to raise_error(ActiveRecord::RecordNotDestroyed)
|
|
167
|
+
|
|
168
|
+
AbortsDestroy.where(type: TemporaryAbortedDestroy.sti_name).delete_all
|
|
130
169
|
end
|
|
170
|
+
end
|
|
131
171
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
172
|
+
context "when its rows have dependent records protected by a foreign key" do
|
|
173
|
+
with_model :HasDependentChildren do
|
|
174
|
+
table do |t|
|
|
175
|
+
t.string :type
|
|
176
|
+
end
|
|
177
|
+
model do
|
|
178
|
+
has_many :dependent_children,
|
|
179
|
+
class_name: "DependentChild",
|
|
180
|
+
foreign_key: :parent_id,
|
|
181
|
+
inverse_of: :parent,
|
|
182
|
+
dependent: :destroy
|
|
183
|
+
end
|
|
184
|
+
end
|
|
136
185
|
|
|
137
|
-
|
|
186
|
+
with_model :DependentChild do
|
|
187
|
+
table do |t|
|
|
188
|
+
t.references :parent,
|
|
189
|
+
null: false,
|
|
190
|
+
foreign_key: {to_table: HasDependentChildren.table_name}
|
|
191
|
+
end
|
|
192
|
+
model do
|
|
193
|
+
belongs_to :parent,
|
|
194
|
+
class_name: "HasDependentChildren",
|
|
195
|
+
inverse_of: :dependent_children
|
|
196
|
+
end
|
|
197
|
+
end
|
|
138
198
|
|
|
139
|
-
|
|
199
|
+
with_model :OtherDependentParent, superclass: :HasDependentChildren do
|
|
200
|
+
table(false)
|
|
140
201
|
end
|
|
141
202
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
203
|
+
after do
|
|
204
|
+
expect(HasDependentChildren.unscoped.ids).to contain_exactly(@superclass.id, @sibling.id)
|
|
205
|
+
expect(DependentChild.pluck(:parent_id)).to contain_exactly(@superclass.id, @sibling.id)
|
|
206
|
+
end
|
|
145
207
|
|
|
146
|
-
|
|
147
|
-
|
|
208
|
+
with_model :TemporaryDependentParent, superclass: :HasDependentChildren do
|
|
209
|
+
table(false)
|
|
210
|
+
end
|
|
148
211
|
|
|
149
|
-
|
|
212
|
+
it "destroys its dependents without removing superclass or sibling rows" do
|
|
213
|
+
temporary = TemporaryDependentParent.create!
|
|
214
|
+
@superclass = HasDependentChildren.create!
|
|
215
|
+
@sibling = OtherDependentParent.create!
|
|
216
|
+
[temporary, @superclass, @sibling].each do |parent|
|
|
217
|
+
parent.dependent_children.create!
|
|
218
|
+
end
|
|
150
219
|
end
|
|
151
220
|
end
|
|
221
|
+
end
|
|
152
222
|
|
|
153
|
-
|
|
154
|
-
|
|
223
|
+
describe "with a superclass that has a default_scope" do
|
|
224
|
+
with_model :HasDefaultScope do
|
|
225
|
+
table do |t|
|
|
155
226
|
t.string :type
|
|
156
227
|
t.boolean :archived, default: false
|
|
157
228
|
end
|
|
229
|
+
model do
|
|
230
|
+
default_scope { where(archived: false) }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
158
233
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
model = WithModel::Model.new(:ScopedChild, superclass: HasDefaultScope)
|
|
163
|
-
WithModel::Model::DSL.new(model).table(false)
|
|
164
|
-
model.create
|
|
165
|
-
ScopedChild.create!(archived: false)
|
|
166
|
-
ScopedChild.create!(archived: true)
|
|
234
|
+
after do
|
|
235
|
+
expect(HasDefaultScope.unscoped.where(type: "ScopedChild").count).to eq 0
|
|
236
|
+
end
|
|
167
237
|
|
|
168
|
-
|
|
238
|
+
with_model :ScopedChild, superclass: :HasDefaultScope do
|
|
239
|
+
table(false)
|
|
240
|
+
end
|
|
169
241
|
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
it "deletes rows the default scope would have hidden" do
|
|
243
|
+
ScopedChild.create!(archived: false)
|
|
244
|
+
ScopedChild.create!(archived: true)
|
|
172
245
|
end
|
|
246
|
+
end
|
|
173
247
|
|
|
174
|
-
|
|
175
|
-
|
|
248
|
+
describe "with a non-standard inheritance_column" do
|
|
249
|
+
with_model :HasCustomInheritanceColumn do
|
|
250
|
+
table do |t|
|
|
176
251
|
t.string :kind
|
|
177
252
|
t.string :name
|
|
178
253
|
end
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
with_model :KindedChild, superclass: HasCustomInheritanceColumn do
|
|
183
|
-
table(false)
|
|
254
|
+
model do
|
|
255
|
+
self.inheritance_column = "kind"
|
|
184
256
|
end
|
|
257
|
+
end
|
|
185
258
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
end
|
|
259
|
+
with_model :KindedChild, superclass: :HasCustomInheritanceColumn do
|
|
260
|
+
table(false)
|
|
261
|
+
end
|
|
190
262
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
263
|
+
it "writes to the custom column" do
|
|
264
|
+
KindedChild.create!(name: "x")
|
|
265
|
+
expect(HasCustomInheritanceColumn.first.kind).to eq "KindedChild"
|
|
194
266
|
end
|
|
195
267
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
268
|
+
it "accepts a Symbol inheritance_column" do
|
|
269
|
+
expect(HasCustomInheritanceColumn.inheritance_column).to eq "kind"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# The anchor case: a validation or teardown that hard-codes "type" passes
|
|
274
|
+
# every other example here and still scopes on the wrong column.
|
|
275
|
+
describe "with both a type column and a custom inheritance_column" do
|
|
276
|
+
with_model :CustomInheritanceWithType do
|
|
277
|
+
table do |t|
|
|
200
278
|
t.string :kind
|
|
201
279
|
t.string :type
|
|
202
280
|
t.string :name
|
|
203
281
|
end
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
with_model :KindedTypeChild, superclass: HasCustomInheritanceColumnAndTypeColumn do
|
|
208
|
-
table(false)
|
|
282
|
+
model do
|
|
283
|
+
self.inheritance_column = "kind"
|
|
209
284
|
end
|
|
285
|
+
end
|
|
210
286
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
row = HasCustomInheritanceColumnAndTypeColumn.first
|
|
214
|
-
expect(row.kind).to eq "KindedTypeChild"
|
|
215
|
-
expect(row.type).to be_nil
|
|
216
|
-
end
|
|
287
|
+
with_model :KindedTypeChild, superclass: :CustomInheritanceWithType do
|
|
288
|
+
table(false)
|
|
217
289
|
end
|
|
218
290
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
291
|
+
it "writes the custom column and leaves type null" do
|
|
292
|
+
KindedTypeChild.create!(name: "x")
|
|
293
|
+
row = CustomInheritanceWithType.first
|
|
294
|
+
expect(row.kind).to eq "KindedTypeChild"
|
|
295
|
+
expect(row.type).to be_nil
|
|
296
|
+
end
|
|
297
|
+
end
|
|
226
298
|
|
|
227
|
-
|
|
228
|
-
|
|
299
|
+
describe "with a with_model superclass" do
|
|
300
|
+
with_model :Parent do
|
|
301
|
+
table do |t|
|
|
302
|
+
t.string :type
|
|
303
|
+
t.string :name
|
|
229
304
|
end
|
|
305
|
+
end
|
|
230
306
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
307
|
+
with_model :StringChild, superclass: "Parent" do
|
|
308
|
+
table(false)
|
|
309
|
+
end
|
|
234
310
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
311
|
+
with_model :SymbolChild, superclass: :Parent do
|
|
312
|
+
table(false)
|
|
313
|
+
end
|
|
238
314
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
end
|
|
315
|
+
with_model :CallableChild, superclass: -> { Parent } do
|
|
316
|
+
table(false)
|
|
317
|
+
end
|
|
243
318
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
end
|
|
319
|
+
it "resolves a String superclass" do
|
|
320
|
+
expect(StringChild.superclass).to eq Parent
|
|
321
|
+
expect(StringChild.table_name).to eq Parent.table_name
|
|
322
|
+
end
|
|
249
323
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
324
|
+
# The same spelling with_model names its own models with.
|
|
325
|
+
it "resolves a Symbol superclass" do
|
|
326
|
+
expect(SymbolChild.superclass).to eq Parent
|
|
327
|
+
expect(SymbolChild.table_name).to eq Parent.table_name
|
|
328
|
+
end
|
|
253
329
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
end
|
|
330
|
+
it "resolves a callable superclass" do
|
|
331
|
+
expect(CallableChild.superclass).to eq Parent
|
|
332
|
+
end
|
|
258
333
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
expect(StringChild.superclass).to eq Parent
|
|
263
|
-
expect(StringChild.superclass).to be Parent
|
|
264
|
-
end
|
|
334
|
+
it "round-trips through the parent" do
|
|
335
|
+
StringChild.create!(name: "x")
|
|
336
|
+
expect(Parent.first).to be_a StringChild
|
|
265
337
|
end
|
|
266
338
|
|
|
267
|
-
|
|
268
|
-
|
|
339
|
+
# Second example: the parent class object is rebuilt every example, so a
|
|
340
|
+
# superclass memoized on the first resolution refers to a dead class here.
|
|
341
|
+
it "resolves against the current parent in a later example" do
|
|
342
|
+
expect(StringChild.superclass).to eq Parent
|
|
343
|
+
expect(StringChild.superclass).to be Parent
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
describe "namespaced constants" do
|
|
348
|
+
with_model :HasTypeColumn do
|
|
349
|
+
table do |t|
|
|
269
350
|
t.string :type
|
|
270
351
|
t.string :name
|
|
271
352
|
end
|
|
353
|
+
end
|
|
272
354
|
|
|
273
|
-
|
|
355
|
+
# WithModel::ConstantStubber resolves a namespaced name with `const_get` and
|
|
356
|
+
# stubs only the last segment, so the namespace has to exist already.
|
|
357
|
+
before { stub_const("TableFalseSpec", Module.new) }
|
|
274
358
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
with_model "TableFalseSpec::Step", superclass: HasTypeColumn do
|
|
279
|
-
table(false)
|
|
280
|
-
end
|
|
359
|
+
with_model "TableFalseSpec::Step", superclass: :HasTypeColumn do
|
|
360
|
+
table(false)
|
|
361
|
+
end
|
|
281
362
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
end
|
|
363
|
+
it "stores and round-trips the fully qualified name" do
|
|
364
|
+
TableFalseSpec::Step.create!(name: "x")
|
|
365
|
+
expect(HasTypeColumn.first.type).to eq "TableFalseSpec::Step"
|
|
366
|
+
expect(HasTypeColumn.first).to be_a TableFalseSpec::Step
|
|
287
367
|
end
|
|
368
|
+
end
|
|
288
369
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
370
|
+
# A missing table is a fact about the schema at that moment, not about the
|
|
371
|
+
# model, so it is allowed: the table may arrive later in the example, or its
|
|
372
|
+
# absence may be the very thing under test.
|
|
373
|
+
describe "when the superclass's table does not exist" do
|
|
374
|
+
# The shape a Rails app's ApplicationRecord takes: abstract, and inherited
|
|
375
|
+
# from rather than ActiveRecord::Base directly.
|
|
376
|
+
with_model :ApplicationRecord do
|
|
377
|
+
table
|
|
378
|
+
model do
|
|
379
|
+
self.abstract_class = true
|
|
380
|
+
self.table_name = nil
|
|
295
381
|
end
|
|
382
|
+
end
|
|
296
383
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
expect(Orphan).to be < HasMissingTable
|
|
301
|
-
expect(Orphan.table_name).to eq HasMissingTable.table_name
|
|
302
|
-
end
|
|
384
|
+
before do
|
|
385
|
+
stub_active_record_class(:HasMissingTable, superclass: ApplicationRecord)
|
|
386
|
+
end
|
|
303
387
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
end
|
|
388
|
+
with_model :Orphan, superclass: -> { HasMissingTable } do
|
|
389
|
+
table(false)
|
|
390
|
+
end
|
|
308
391
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
392
|
+
it "still defines the model, which inherits the missing table's name" do
|
|
393
|
+
# Not `new`: building an instance reads column information, which is
|
|
394
|
+
# itself a use of the absent table.
|
|
395
|
+
expect(Orphan).to be < HasMissingTable
|
|
396
|
+
expect(Orphan.table_name).to eq HasMissingTable.table_name
|
|
314
397
|
end
|
|
315
398
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
399
|
+
it "lets Active Record raise its own error, naming the table" do
|
|
400
|
+
expect { Orphan.create!(name: "x") }
|
|
401
|
+
.to raise_error(ActiveRecord::StatementInvalid, /#{HasMissingTable.table_name}/)
|
|
402
|
+
end
|
|
320
403
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
404
|
+
it "tears down without failing an example that otherwise passed" do
|
|
405
|
+
# Reaching the end of this example at all is the assertion: teardown has
|
|
406
|
+
# no rows to delete and must not go looking for them.
|
|
407
|
+
expect(Orphan.name).to eq "Orphan"
|
|
408
|
+
end
|
|
409
|
+
end
|
|
324
410
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
t.string :type
|
|
328
|
-
t.string :name
|
|
329
|
-
end
|
|
330
|
-
HasLateTable.reset_column_information
|
|
411
|
+
describe "when the superclass's table is created during the example" do
|
|
412
|
+
before { stub_active_record_class(:HasLateTable) }
|
|
331
413
|
|
|
332
|
-
|
|
414
|
+
with_model :LateChild, superclass: -> { HasLateTable } do
|
|
415
|
+
table(false)
|
|
416
|
+
end
|
|
333
417
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
end
|
|
418
|
+
after do
|
|
419
|
+
ActiveRecord::Base.connection.drop_table HasLateTable.table_name, if_exists: true
|
|
337
420
|
end
|
|
338
421
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
model.create
|
|
422
|
+
it "inherits the table once it exists" do
|
|
423
|
+
ActiveRecord::Base.connection.create_table HasLateTable.table_name, force: true do |t|
|
|
424
|
+
t.string :type
|
|
425
|
+
t.string :name
|
|
344
426
|
end
|
|
427
|
+
HasLateTable.reset_column_information
|
|
428
|
+
|
|
429
|
+
LateChild.create!(name: "late")
|
|
430
|
+
|
|
431
|
+
expect(HasLateTable.count).to eq 1
|
|
432
|
+
expect(HasLateTable.first).to be_a LateChild
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
describe "invalid configurations" do
|
|
437
|
+
# Creates immediately the same tableless subclass normally generated by:
|
|
438
|
+
#
|
|
439
|
+
# with_model :TablelessSubclass, superclass: superclass do
|
|
440
|
+
# table(false)
|
|
441
|
+
# end
|
|
442
|
+
#
|
|
443
|
+
# Bypassing `with_model`'s setup hook lets these examples assert failures
|
|
444
|
+
# raised while the superclass is resolved or the table configuration checked.
|
|
445
|
+
def create_tableless_subclass(superclass:, &dsl)
|
|
446
|
+
model = WithModel::Model.new(:TablelessSubclass, superclass: superclass)
|
|
447
|
+
WithModel::Model::DSL.new(model).instance_eval(&dsl || proc { table(false) })
|
|
448
|
+
model.create
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it "names the model, which the superclass's name does not identify" do
|
|
452
|
+
expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
|
|
453
|
+
.to raise_error(
|
|
454
|
+
WithModel::InvalidSuperclass,
|
|
455
|
+
/with_model :TablelessSubclass has no table of its own/
|
|
456
|
+
)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it "raises WithModel::InvalidSuperclass for ActiveRecord::Base" do
|
|
460
|
+
expect { create_tableless_subclass(superclass: ActiveRecord::Base) }
|
|
461
|
+
.to raise_error(WithModel::InvalidSuperclass, /ActiveRecord::Base has none to inherit/)
|
|
462
|
+
end
|
|
345
463
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
464
|
+
context "with an abstract superclass" do
|
|
465
|
+
with_model :TablelessSuperclass do
|
|
466
|
+
table
|
|
467
|
+
model do
|
|
468
|
+
self.abstract_class = true
|
|
469
|
+
self.table_name = nil
|
|
470
|
+
end
|
|
349
471
|
end
|
|
350
472
|
|
|
351
|
-
it "
|
|
352
|
-
expect {
|
|
353
|
-
.to raise_error(WithModel::InvalidSuperclass, /
|
|
473
|
+
it "raises WithModel::InvalidSuperclass" do
|
|
474
|
+
expect { create_tableless_subclass(superclass: TablelessSuperclass) }
|
|
475
|
+
.to raise_error(WithModel::InvalidSuperclass, /TablelessSuperclass has none to inherit/)
|
|
354
476
|
end
|
|
477
|
+
end
|
|
355
478
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
479
|
+
context "with an abstract superclass a Rails app would call ApplicationRecord" do
|
|
480
|
+
with_model :ApplicationRecord do
|
|
481
|
+
table
|
|
482
|
+
model do
|
|
483
|
+
self.abstract_class = true
|
|
484
|
+
self.table_name = nil
|
|
485
|
+
end
|
|
359
486
|
end
|
|
360
487
|
|
|
361
|
-
it "
|
|
362
|
-
expect {
|
|
488
|
+
it "raises WithModel::InvalidSuperclass" do
|
|
489
|
+
expect { create_tableless_subclass(superclass: ApplicationRecord) }
|
|
363
490
|
.to raise_error(WithModel::InvalidSuperclass, /ApplicationRecord has none to inherit/)
|
|
364
491
|
end
|
|
492
|
+
end
|
|
365
493
|
|
|
366
|
-
|
|
367
|
-
|
|
494
|
+
context "when the superclass table lacks the inheritance column" do
|
|
495
|
+
with_model :HasNoTypeColumn do
|
|
496
|
+
table do |t|
|
|
368
497
|
t.string :name
|
|
369
498
|
end
|
|
499
|
+
end
|
|
370
500
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
expect { create_model(superclass: HasNoTypeColumn) }
|
|
375
|
-
.to raise_error(WithModel::InvalidSuperclass, /"type" column/)
|
|
376
|
-
end
|
|
501
|
+
it "raises WithModel::InvalidSuperclass and names the missing column" do
|
|
502
|
+
expect { create_tableless_subclass(superclass: HasNoTypeColumn) }
|
|
503
|
+
.to raise_error(WithModel::InvalidSuperclass, /"type" column/)
|
|
377
504
|
end
|
|
505
|
+
end
|
|
378
506
|
|
|
379
|
-
|
|
380
|
-
|
|
507
|
+
context "when the superclass disables inheritance" do
|
|
508
|
+
with_model :HasNilInheritanceColumn do
|
|
509
|
+
table do |t|
|
|
381
510
|
t.string :name
|
|
382
511
|
end
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
expect { create_model(superclass: HasNilInheritanceColumn) }
|
|
386
|
-
.to raise_error(WithModel::InvalidSuperclass)
|
|
512
|
+
model do
|
|
513
|
+
self.inheritance_column = nil
|
|
387
514
|
end
|
|
388
515
|
end
|
|
389
516
|
|
|
390
|
-
it "
|
|
391
|
-
expect
|
|
392
|
-
|
|
393
|
-
end.to raise_error(ArgumentError, /table does not take a block when its first argument is falsy/)
|
|
517
|
+
it "raises WithModel::InvalidSuperclass" do
|
|
518
|
+
expect { create_tableless_subclass(superclass: HasNilInheritanceColumn) }
|
|
519
|
+
.to raise_error(WithModel::InvalidSuperclass)
|
|
394
520
|
end
|
|
521
|
+
end
|
|
395
522
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
it "names the segment of a namespaced superclass that is missing" do
|
|
404
|
-
expect { create_model(superclass: "NoSuchNamespace::Deep::Parent") }
|
|
405
|
-
.to raise_error(WithModel::MissingSuperclass, /uninitialized constant NoSuchNamespace\./)
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
it "reports an unresolvable Symbol as the Symbol that was passed" do
|
|
409
|
-
expect { create_model(superclass: :NoSuchParent) }
|
|
410
|
-
.to raise_error(WithModel::MissingSuperclass,
|
|
411
|
-
/superclass :NoSuchParent could not be resolved: uninitialized constant NoSuchParent/)
|
|
523
|
+
context "when table(false) is passed a block" do
|
|
524
|
+
with_model :HasTypeColumn do
|
|
525
|
+
table do |t|
|
|
526
|
+
t.string :type
|
|
527
|
+
t.string :name
|
|
412
528
|
end
|
|
529
|
+
end
|
|
413
530
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.to raise_error(WithModel::MissingSuperclass) { |error|
|
|
419
|
-
expect(error.cause).to be_a NameError
|
|
420
|
-
}
|
|
421
|
-
end
|
|
531
|
+
it "raises ArgumentError" do
|
|
532
|
+
expect do
|
|
533
|
+
create_tableless_subclass(superclass: HasTypeColumn) { table(false) { |t| t.string :nope } }
|
|
534
|
+
end.to raise_error(ArgumentError, /table does not take a block when its first argument is falsy/)
|
|
422
535
|
end
|
|
536
|
+
end
|
|
423
537
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
expect(
|
|
538
|
+
describe "a superclass that cannot be found" do
|
|
539
|
+
it "quotes the reason it could not be resolved" do
|
|
540
|
+
expect { create_tableless_subclass(superclass: "NoSuchParent") }
|
|
541
|
+
.to raise_error(WithModel::MissingSuperclass,
|
|
542
|
+
/superclass "NoSuchParent" could not be resolved: uninitialized constant NoSuchParent/)
|
|
427
543
|
end
|
|
428
544
|
|
|
429
|
-
it "
|
|
430
|
-
expect {
|
|
431
|
-
.to raise_error(WithModel::
|
|
432
|
-
/name it with a String or a Symbol, or pass a callable returning it/)
|
|
545
|
+
it "names the segment of a namespaced superclass that is missing" do
|
|
546
|
+
expect { create_tableless_subclass(superclass: "NoSuchNamespace::Deep::Parent") }
|
|
547
|
+
.to raise_error(WithModel::MissingSuperclass, /uninitialized constant NoSuchNamespace\./)
|
|
433
548
|
end
|
|
434
549
|
|
|
435
|
-
it "
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
550
|
+
it "reports an unresolvable Symbol as the Symbol that was passed" do
|
|
551
|
+
expect { create_tableless_subclass(superclass: :NoSuchParent) }
|
|
552
|
+
.to raise_error(WithModel::MissingSuperclass,
|
|
553
|
+
/superclass :NoSuchParent could not be resolved: uninitialized constant NoSuchParent/)
|
|
439
554
|
end
|
|
440
555
|
|
|
441
|
-
it "
|
|
442
|
-
|
|
443
|
-
|
|
556
|
+
it "keeps the NameError as its cause" do
|
|
557
|
+
# Ruby sets this for a raise inside a rescue, so the original backtrace
|
|
558
|
+
# stays reachable.
|
|
559
|
+
expect { create_tableless_subclass(superclass: "NoSuchParent") }
|
|
560
|
+
.to raise_error(WithModel::MissingSuperclass) { |error|
|
|
561
|
+
expect(error.cause).to be_a NameError
|
|
562
|
+
}
|
|
444
563
|
end
|
|
445
564
|
end
|
|
446
|
-
end
|
|
447
565
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
WithModel.deprecator.behavior = ->(message, *) { collected << message }
|
|
452
|
-
yield
|
|
453
|
-
collected
|
|
454
|
-
ensure
|
|
455
|
-
WithModel.deprecator.behavior = :raise
|
|
566
|
+
it "tells a missing superclass apart from an unusable one, but catches both" do
|
|
567
|
+
expect(WithModel::MissingSuperclass).to be < WithModel::InvalidSuperclass
|
|
568
|
+
expect(WithModel::InvalidSuperclass).to be < ArgumentError
|
|
456
569
|
end
|
|
457
570
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
571
|
+
it "says how to name a class that is not defined yet" do
|
|
572
|
+
expect { create_tableless_subclass(superclass: 42) }
|
|
573
|
+
.to raise_error(WithModel::InvalidSuperclass,
|
|
574
|
+
/name it with a String or a Symbol, or pass a callable returning it/)
|
|
462
575
|
end
|
|
463
576
|
|
|
464
|
-
it "
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
expect(messages.join).to match(/table/)
|
|
577
|
+
it "raises WithModel::InvalidSuperclass for a value that only has a to_s" do
|
|
578
|
+
# Every object has one, so honoring it would look up the constant "42".
|
|
579
|
+
expect { create_tableless_subclass(superclass: 42) }
|
|
580
|
+
.to raise_error(WithModel::InvalidSuperclass, /but was 42/)
|
|
469
581
|
end
|
|
470
582
|
|
|
471
|
-
it "
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
end
|
|
475
|
-
expect(messages.join).to include("3.0")
|
|
583
|
+
it "raises WithModel::InvalidSuperclass for a callable returning a non-ActiveRecord value" do
|
|
584
|
+
expect { create_tableless_subclass(superclass: -> { 42 }) }
|
|
585
|
+
.to raise_error(WithModel::InvalidSuperclass, /but was 42/)
|
|
476
586
|
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
477
589
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
messages = capture_deprecations do
|
|
483
|
-
define_group { with_model(:Warned) }
|
|
484
|
-
end
|
|
590
|
+
RSpec.describe "omitting table" do
|
|
591
|
+
before do
|
|
592
|
+
allow(WithModel.deprecator).to receive(:warn)
|
|
593
|
+
end
|
|
485
594
|
|
|
486
|
-
|
|
487
|
-
|
|
595
|
+
# Defines the supplied block as if it had appeared in an isolated group:
|
|
596
|
+
#
|
|
597
|
+
# RSpec.describe "throwaway" do
|
|
598
|
+
# # supplied definitions
|
|
599
|
+
# end
|
|
600
|
+
#
|
|
601
|
+
# The warning happens at definition time, so the assertion cannot rely on this
|
|
602
|
+
# file's own example-group load order.
|
|
603
|
+
def define_group(&body)
|
|
604
|
+
RSpec::Core::ExampleGroup.describe("throwaway", &body)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it "warns when no table is specified" do
|
|
608
|
+
define_group { with_model(:Warned) }
|
|
609
|
+
|
|
610
|
+
expect(WithModel.deprecator).to have_received(:warn) do |message, _callstack|
|
|
611
|
+
expect(message).to match(/table/)
|
|
488
612
|
end
|
|
613
|
+
end
|
|
489
614
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
expect(
|
|
615
|
+
it "names the 3.0 horizon in the warning" do
|
|
616
|
+
define_group { with_model(:Warned) }
|
|
617
|
+
|
|
618
|
+
expect(WithModel.deprecator).to have_received(:warn) do |message, _callstack|
|
|
619
|
+
expect(message).to include("3.0")
|
|
495
620
|
end
|
|
621
|
+
end
|
|
496
622
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
623
|
+
# Left to itself ActiveSupport::Deprecation blames the first frame it does not
|
|
624
|
+
# recognize as Rails or the standard library, which is with_model's own source
|
|
625
|
+
# - the same line for every omission in a suite.
|
|
626
|
+
it "blames the with_model call rather than with_model's own source" do
|
|
627
|
+
expected_source_line = __LINE__ + 1
|
|
628
|
+
define_group { with_model(:Warned) }
|
|
629
|
+
|
|
630
|
+
expect(WithModel.deprecator).to have_received(:warn) do |_message, callstack|
|
|
631
|
+
expect(callstack.first.path).to eq(__FILE__)
|
|
632
|
+
expect(callstack.first.lineno).to eq(expected_source_line)
|
|
633
|
+
expect(callstack.first.path).not_to include("lib/with_model.rb")
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
context "when table(false) is used" do
|
|
638
|
+
with_model :HasTypeColumn do
|
|
639
|
+
table do |t|
|
|
640
|
+
t.string :type
|
|
641
|
+
t.string :name
|
|
505
642
|
end
|
|
506
|
-
expect(messages).to be_empty
|
|
507
643
|
end
|
|
508
644
|
|
|
509
|
-
it "does not warn
|
|
510
|
-
|
|
511
|
-
|
|
645
|
+
it "does not warn" do
|
|
646
|
+
define_group { with_model(:Quiet, superclass: HasTypeColumn) { table(false) } }
|
|
647
|
+
|
|
648
|
+
expect(WithModel.deprecator).not_to have_received(:warn)
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
it "does not warn for an empty table block" do
|
|
653
|
+
define_group do
|
|
654
|
+
with_model(:Quiet) do
|
|
655
|
+
table do
|
|
656
|
+
end
|
|
512
657
|
end
|
|
513
|
-
expect(messages).to be_empty
|
|
514
658
|
end
|
|
659
|
+
|
|
660
|
+
expect(WithModel.deprecator).not_to have_received(:warn)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it "does not warn for a `table` call with no arguments" do
|
|
664
|
+
define_group { with_model(:Quiet) { table } }
|
|
665
|
+
|
|
666
|
+
expect(WithModel.deprecator).not_to have_received(:warn)
|
|
515
667
|
end
|
|
516
668
|
end
|