pod4 0.6.2
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 +7 -0
- data/.hgignore +18 -0
- data/.hgtags +19 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +556 -0
- data/Rakefile +30 -0
- data/lib/pod4/alert.rb +87 -0
- data/lib/pod4/basic_model.rb +137 -0
- data/lib/pod4/errors.rb +80 -0
- data/lib/pod4/interface.rb +110 -0
- data/lib/pod4/metaxing.rb +66 -0
- data/lib/pod4/model.rb +347 -0
- data/lib/pod4/nebulous_interface.rb +408 -0
- data/lib/pod4/null_interface.rb +148 -0
- data/lib/pod4/param.rb +29 -0
- data/lib/pod4/pg_interface.rb +460 -0
- data/lib/pod4/sequel_interface.rb +303 -0
- data/lib/pod4/tds_interface.rb +394 -0
- data/lib/pod4/version.rb +3 -0
- data/lib/pod4.rb +54 -0
- data/md/fixme.md +32 -0
- data/md/roadmap.md +69 -0
- data/pod4.gemspec +49 -0
- data/spec/README.md +19 -0
- data/spec/alert_spec.rb +173 -0
- data/spec/basic_model_spec.rb +220 -0
- data/spec/doc_no_pending.rb +5 -0
- data/spec/fixtures/database.rb +13 -0
- data/spec/model_spec.rb +760 -0
- data/spec/nebulous_interface_spec.rb +286 -0
- data/spec/null_interface_spec.rb +153 -0
- data/spec/param_spec.rb +89 -0
- data/spec/pg_interface_spec.rb +452 -0
- data/spec/pod4_spec.rb +88 -0
- data/spec/sequel_interface_spec.rb +466 -0
- data/spec/shared_examples_for_interface.rb +160 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/tds_interface_spec.rb +494 -0
- data/tags +106 -0
- metadata +316 -0
@@ -0,0 +1,452 @@
|
|
1
|
+
require 'pod4/pg_interface'
|
2
|
+
|
3
|
+
require_relative 'shared_examples_for_interface'
|
4
|
+
require_relative 'fixtures/database'
|
5
|
+
|
6
|
+
|
7
|
+
class TestPgInterface < PgInterface
|
8
|
+
set_table :customer
|
9
|
+
set_id_fld :id
|
10
|
+
end
|
11
|
+
|
12
|
+
class SchemaPgInterface < PgInterface
|
13
|
+
set_schema :public
|
14
|
+
set_table :customer
|
15
|
+
set_id_fld :id
|
16
|
+
end
|
17
|
+
|
18
|
+
class BadPgInterface1 < PgInterface
|
19
|
+
set_table :customer
|
20
|
+
end
|
21
|
+
|
22
|
+
class BadPgInterface2 < PgInterface
|
23
|
+
set_id_fld :id
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
describe TestPgInterface do
|
28
|
+
|
29
|
+
def db_setup(connect)
|
30
|
+
client = PG.connect(connect)
|
31
|
+
|
32
|
+
client.exec(%Q|drop table if exists customer;|)
|
33
|
+
|
34
|
+
client.exec(%Q|
|
35
|
+
create table customer (
|
36
|
+
id serial,
|
37
|
+
name text,
|
38
|
+
level real null,
|
39
|
+
day date null,
|
40
|
+
timestamp timestamp null,
|
41
|
+
price money null,
|
42
|
+
qty numeric null );| )
|
43
|
+
|
44
|
+
ensure
|
45
|
+
client.finish if client
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def fill_data(ifce)
|
50
|
+
@data.each{|r| ifce.create(r) }
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
before(:all) do
|
55
|
+
@connect_hash = DB[:pg]
|
56
|
+
db_setup(@connect_hash)
|
57
|
+
|
58
|
+
@data = []
|
59
|
+
@data << { name: 'Barney',
|
60
|
+
level: 1.23,
|
61
|
+
day: Date.parse("2016-01-01"),
|
62
|
+
timestamp: Time.parse('2015-01-01 12:11'),
|
63
|
+
price: BigDecimal.new("1.24"),
|
64
|
+
qty: BigDecimal.new("1.25") }
|
65
|
+
|
66
|
+
@data << { name: 'Fred',
|
67
|
+
level: 2.34,
|
68
|
+
day: Date.parse("2016-02-02"),
|
69
|
+
timestamp: Time.parse('2015-01-02 12:22'),
|
70
|
+
price: BigDecimal.new("2.35"),
|
71
|
+
qty: BigDecimal.new("2.36") }
|
72
|
+
|
73
|
+
@data << { name: 'Betty',
|
74
|
+
level: 3.45,
|
75
|
+
day: Date.parse("2016-03-03"),
|
76
|
+
timestamp: Time.parse('2015-01-03 12:33'),
|
77
|
+
price: BigDecimal.new("3.46"),
|
78
|
+
qty: BigDecimal.new("3.47") }
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
before do
|
84
|
+
# TRUNCATE TABLE also resets the identity counter
|
85
|
+
interface.execute(%Q|truncate table customer restart identity;|)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
let(:interface) do
|
90
|
+
TestPgInterface.new(@connect_hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
#####
|
94
|
+
|
95
|
+
|
96
|
+
it_behaves_like 'an interface' do
|
97
|
+
|
98
|
+
let(:interface) do
|
99
|
+
TestPgInterface.new(@connect_hash)
|
100
|
+
end
|
101
|
+
|
102
|
+
let(:record) { {name: 'Barney'} }
|
103
|
+
|
104
|
+
end
|
105
|
+
##
|
106
|
+
|
107
|
+
|
108
|
+
describe 'PgInterface.set_schema' do
|
109
|
+
it 'takes one argument' do
|
110
|
+
expect( PgInterface ).to respond_to(:set_schema).with(1).argument
|
111
|
+
end
|
112
|
+
end
|
113
|
+
##
|
114
|
+
|
115
|
+
|
116
|
+
describe 'PgInterface.schema' do
|
117
|
+
it 'returns the schema' do
|
118
|
+
expect( SchemaPgInterface.schema ).to eq :public
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'is optional' do
|
122
|
+
expect{ TestPgInterface.schema }.not_to raise_exception
|
123
|
+
expect( TestPgInterface.schema ).to eq nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
##
|
127
|
+
|
128
|
+
|
129
|
+
describe 'PgInterface.set_table' do
|
130
|
+
it 'takes one argument' do
|
131
|
+
expect( PgInterface ).to respond_to(:set_table).with(1).argument
|
132
|
+
end
|
133
|
+
end
|
134
|
+
##
|
135
|
+
|
136
|
+
|
137
|
+
describe 'PgInterface.table' do
|
138
|
+
it 'returns the table' do
|
139
|
+
expect( TestPgInterface.table ).to eq :customer
|
140
|
+
end
|
141
|
+
end
|
142
|
+
##
|
143
|
+
|
144
|
+
|
145
|
+
describe 'PgInterface.set_id_fld' do
|
146
|
+
it 'takes one argument' do
|
147
|
+
expect( PgInterface ).to respond_to(:set_id_fld).with(1).argument
|
148
|
+
end
|
149
|
+
end
|
150
|
+
##
|
151
|
+
|
152
|
+
|
153
|
+
describe 'PgInterface.id_fld' do
|
154
|
+
it 'returns the ID field name' do
|
155
|
+
expect( TestPgInterface.id_fld ).to eq :id
|
156
|
+
end
|
157
|
+
end
|
158
|
+
##
|
159
|
+
|
160
|
+
|
161
|
+
describe '#new' do
|
162
|
+
|
163
|
+
it 'requires a TinyTds connection string' do
|
164
|
+
expect{ TestPgInterface.new }.to raise_exception ArgumentError
|
165
|
+
expect{ TestPgInterface.new(nil) }.to raise_exception ArgumentError
|
166
|
+
expect{ TestPgInterface.new('foo') }.to raise_exception ArgumentError
|
167
|
+
|
168
|
+
expect{ TestPgInterface.new(@connect_hash) }.not_to raise_exception
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
##
|
173
|
+
|
174
|
+
|
175
|
+
describe '#quoted_table' do
|
176
|
+
|
177
|
+
it 'returns just the table when the schema is not set' do
|
178
|
+
expect( interface.quoted_table ).to eq( %Q|"customer"| )
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'returns the schema plus table when the schema is set' do
|
182
|
+
ifce = SchemaPgInterface.new(@connect_hash)
|
183
|
+
expect( ifce.quoted_table ).to eq( %|"public"."customer"| )
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
##
|
188
|
+
|
189
|
+
|
190
|
+
describe '#create' do
|
191
|
+
|
192
|
+
let(:hash) { {name: 'Bam-Bam', price: 4.44} }
|
193
|
+
let(:ot) { Octothorpe.new(name: 'Wilma', price: 5.55) }
|
194
|
+
|
195
|
+
it 'raises a Pod4::DatabaseError if anything goes wrong' do
|
196
|
+
expect{ interface.create(one: 'two') }.to raise_exception DatabaseError
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'creates the record when given a hash' do
|
200
|
+
# kinda impossible to seperate these two tests
|
201
|
+
id = interface.create(hash)
|
202
|
+
|
203
|
+
expect{ interface.read(id) }.not_to raise_exception
|
204
|
+
expect( interface.read(id).to_h ).to include hash
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'creates the record when given an Octothorpe' do
|
208
|
+
id = interface.create(ot)
|
209
|
+
|
210
|
+
expect{ interface.read(id) }.not_to raise_exception
|
211
|
+
expect( interface.read(id).to_h ).to include ot.to_h
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'shouldnt have a problem with record values of nil' do
|
215
|
+
record = {name: 'Ranger', price: nil}
|
216
|
+
expect{ interface.create(record) }.not_to raise_exception
|
217
|
+
id = interface.create(record)
|
218
|
+
expect( interface.read(id).to_h ).to include(record)
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
##
|
223
|
+
|
224
|
+
|
225
|
+
describe '#read' do
|
226
|
+
before { fill_data(interface) }
|
227
|
+
|
228
|
+
it 'returns the record for the id as an Octothorpe' do
|
229
|
+
rec = interface.read(2)
|
230
|
+
expect( rec ).to be_a_kind_of Octothorpe
|
231
|
+
expect( rec.>>.name ).to eq 'Fred'
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'raises a Pod4::CantContinue if the ID is bad' do
|
235
|
+
expect{ interface.read(:foo) }.to raise_exception CantContinue
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'returns an empty Octothorpe if no record matches the ID' do
|
239
|
+
expect{ interface.read(99) }.not_to raise_exception
|
240
|
+
expect( interface.read(99) ).to be_a_kind_of Octothorpe
|
241
|
+
expect( interface.read(99) ).to be_empty
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'returns real fields as Float' do
|
245
|
+
level = interface.read(1).>>.level
|
246
|
+
|
247
|
+
expect( level ).to be_a_kind_of Float
|
248
|
+
expect( level ).to be_within(0.001).of( @data.first[:level] )
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'returns date fields as Date' do
|
252
|
+
date = interface.read(1).>>.day
|
253
|
+
|
254
|
+
expect( date ).to be_a_kind_of Date
|
255
|
+
expect( date ).to eq @data.first[:day]
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'returns datetime fields as Time' do
|
259
|
+
timestamp = interface.read(1).>>.timestamp
|
260
|
+
|
261
|
+
expect( timestamp ).to be_a_kind_of Time
|
262
|
+
expect( timestamp ).to eq @data.first[:timestamp]
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'returns numeric fields as BigDecimal' do
|
266
|
+
qty = interface.read(1).>>.qty
|
267
|
+
|
268
|
+
expect( qty ).to be_a_kind_of BigDecimal
|
269
|
+
expect( qty ).to eq @data.first[:qty]
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'returns money fields as BigDecimal' do
|
273
|
+
price = interface.read(1).>>.price
|
274
|
+
|
275
|
+
expect( price ).to be_a_kind_of BigDecimal
|
276
|
+
expect( price ).to eq @data.first[:price]
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
##
|
281
|
+
|
282
|
+
|
283
|
+
describe '#list' do
|
284
|
+
before { fill_data(interface) }
|
285
|
+
|
286
|
+
it 'has an optional selection parameter, a hash' do
|
287
|
+
# Actually it does not have to be a hash, but FTTB we only support that.
|
288
|
+
expect{ interface.list(name: 'Barney') }.not_to raise_exception
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'returns an array of Octothorpes that match the records' do
|
292
|
+
# convert each OT to a hash and remove the ID key
|
293
|
+
arr = interface.list.map {|ot| x = ot.to_h; x.delete(:id); x }
|
294
|
+
|
295
|
+
expect( arr ).to match_array @data
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'returns a subset of records based on the selection parameter' do
|
299
|
+
expect( interface.list(name: 'Fred').size ).to eq 1
|
300
|
+
|
301
|
+
expect( interface.list(name: 'Betty').first.to_h ).
|
302
|
+
to include(name: 'Betty')
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'returns an empty Array if nothing matches' do
|
307
|
+
expect( interface.list(name: 'Yogi') ).to eq([])
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'raises ArgumentError if the selection criteria is nonsensical' do
|
311
|
+
expect{ interface.list('foo') }.to raise_exception ArgumentError
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
##
|
316
|
+
|
317
|
+
|
318
|
+
describe '#update' do
|
319
|
+
before { fill_data(interface) }
|
320
|
+
|
321
|
+
let(:id) { interface.list.first[:id] }
|
322
|
+
|
323
|
+
def float_price(row)
|
324
|
+
row[:price] = row[:price].to_f
|
325
|
+
row
|
326
|
+
end
|
327
|
+
|
328
|
+
it 'updates the record at ID with record parameter' do
|
329
|
+
record = {name: 'Booboo', price: 99.99}
|
330
|
+
interface.update(id, record)
|
331
|
+
|
332
|
+
# It so happens that TinyTds returns money as BigDecimal --
|
333
|
+
# this is a really good thing, even though it screws with our test.
|
334
|
+
expect( float_price( interface.read(id).to_h ) ).to include(record)
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'raises a CantContinue if anything weird happens with the ID' do
|
338
|
+
expect{ interface.update(99, name: 'Booboo') }.
|
339
|
+
to raise_exception CantContinue
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'raises a DatabaseError if anything weird happens with the record' do
|
344
|
+
expect{ interface.update(id, smarts: 'more') }.
|
345
|
+
to raise_exception DatabaseError
|
346
|
+
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'shouldnt have a problem with record values of nil' do
|
350
|
+
record = {name: 'Ranger', price: nil}
|
351
|
+
expect{ interface.update(id, record) }.not_to raise_exception
|
352
|
+
expect( interface.read(id).to_h ).to include(record)
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
##
|
357
|
+
|
358
|
+
|
359
|
+
describe '#delete' do
|
360
|
+
|
361
|
+
def list_contains(id)
|
362
|
+
interface.list.find {|x| x[interface.id_fld] == id }
|
363
|
+
end
|
364
|
+
|
365
|
+
let(:id) { interface.list.first[:id] }
|
366
|
+
|
367
|
+
before { fill_data(interface) }
|
368
|
+
|
369
|
+
it 'raises CantContinue if anything hinky happens with the id' do
|
370
|
+
expect{ interface.delete(:foo) }.to raise_exception CantContinue
|
371
|
+
expect{ interface.delete(99) }.to raise_exception CantContinue
|
372
|
+
end
|
373
|
+
|
374
|
+
it 'makes the record at ID go away' do
|
375
|
+
expect( list_contains(id) ).to be_truthy
|
376
|
+
interface.delete(id)
|
377
|
+
expect( list_contains(id) ).to be_falsy
|
378
|
+
end
|
379
|
+
|
380
|
+
end
|
381
|
+
##
|
382
|
+
|
383
|
+
|
384
|
+
describe '#execute' do
|
385
|
+
|
386
|
+
let(:sql) { 'delete from customer where cast(price as numeric) < 2.0;' }
|
387
|
+
|
388
|
+
before { fill_data(interface) }
|
389
|
+
|
390
|
+
it 'requires an SQL string' do
|
391
|
+
expect{ interface.execute }.to raise_exception ArgumentError
|
392
|
+
expect{ interface.execute(nil) }.to raise_exception ArgumentError
|
393
|
+
expect{ interface.execute(14) }.to raise_exception ArgumentError
|
394
|
+
end
|
395
|
+
|
396
|
+
it 'raises some sort of Pod4 error if it runs into problems' do
|
397
|
+
expect{ interface.execute('delete from not_a_table') }.
|
398
|
+
to raise_exception Pod4Error
|
399
|
+
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'executes the string' do
|
403
|
+
expect{ interface.execute(sql) }.not_to raise_exception
|
404
|
+
expect( interface.list.size ).to eq(@data.size - 1)
|
405
|
+
expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
409
|
+
##
|
410
|
+
|
411
|
+
|
412
|
+
describe '#select' do
|
413
|
+
|
414
|
+
before { fill_data(interface) }
|
415
|
+
|
416
|
+
it 'requires an SQL string' do
|
417
|
+
expect{ interface.select }.to raise_exception ArgumentError
|
418
|
+
expect{ interface.select(nil) }.to raise_exception ArgumentError
|
419
|
+
expect{ interface.select(14) }.to raise_exception ArgumentError
|
420
|
+
end
|
421
|
+
|
422
|
+
it 'raises some sort of Pod4 error if it runs into problems' do
|
423
|
+
expect{ interface.select('select * from not_a_table') }.
|
424
|
+
to raise_exception Pod4Error
|
425
|
+
|
426
|
+
end
|
427
|
+
|
428
|
+
it 'returns the result of the sql' do
|
429
|
+
sql1 = 'select name from customer where cast(price as numeric) < 2.0;'
|
430
|
+
sql2 = 'select name from customer where cast(price as numeric) < 0.0;'
|
431
|
+
|
432
|
+
expect{ interface.select(sql1) }.not_to raise_exception
|
433
|
+
expect( interface.select(sql1) ).to eq( [{name: 'Barney'}] )
|
434
|
+
expect( interface.select(sql2) ).to eq( [] )
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'works if you pass a non-select' do
|
438
|
+
# By which I mean: still executes the SQL; returns []
|
439
|
+
sql = 'delete from customer where cast(price as numeric) < 2.0;'
|
440
|
+
ret = interface.select(sql)
|
441
|
+
|
442
|
+
expect( interface.list.size ).to eq(@data.size - 1)
|
443
|
+
expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
|
444
|
+
expect( ret ).to eq( [] )
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
448
|
+
##
|
449
|
+
|
450
|
+
|
451
|
+
end
|
452
|
+
|
data/spec/pod4_spec.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require 'pod4'
|
4
|
+
require 'pod4/param'
|
5
|
+
require 'pod4/errors'
|
6
|
+
|
7
|
+
|
8
|
+
describe Pod4 do
|
9
|
+
|
10
|
+
# Magically replaces the real Param module
|
11
|
+
let(:param) { class_double(Pod4::Param).as_stubbed_const }
|
12
|
+
|
13
|
+
after(:all) { Param.reset }
|
14
|
+
|
15
|
+
|
16
|
+
it 'has a version' do
|
17
|
+
expect( Pod4::VERSION ).not_to be_nil
|
18
|
+
end
|
19
|
+
##
|
20
|
+
|
21
|
+
|
22
|
+
describe "Pod4.set_logger" do
|
23
|
+
|
24
|
+
it "calls Param.set" do
|
25
|
+
l = Logger.new(STDOUT)
|
26
|
+
expect(param).to receive(:set).with(:logger, l)
|
27
|
+
Pod4.set_logger(l)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
##
|
32
|
+
|
33
|
+
|
34
|
+
describe 'Pod4.logger' do
|
35
|
+
|
36
|
+
it 'returns the logger as set' do
|
37
|
+
l = Logger.new(STDOUT)
|
38
|
+
Pod4.set_logger(l)
|
39
|
+
|
40
|
+
expect( Pod4.logger ).to eq l
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'still works if no-one set the logger' do
|
44
|
+
expect{ Pod4.logger }.not_to raise_exception
|
45
|
+
expect( Pod4.logger ).to be_a_kind_of Logger
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
##
|
50
|
+
|
51
|
+
|
52
|
+
describe 'Pod4::NotImplemented' do
|
53
|
+
it 'is an exception' do
|
54
|
+
expect( Pod4::NotImplemented ).not_to be_nil
|
55
|
+
expect( Pod4::NotImplemented.ancestors ).to include Exception
|
56
|
+
end
|
57
|
+
end
|
58
|
+
##
|
59
|
+
|
60
|
+
|
61
|
+
describe 'Pod4::Pod4Error' do
|
62
|
+
it 'is an exception' do
|
63
|
+
expect( Pod4::Pod4Error ).not_to be_nil
|
64
|
+
expect( Pod4::Pod4Error.ancestors ).to include Exception
|
65
|
+
end
|
66
|
+
end
|
67
|
+
##
|
68
|
+
|
69
|
+
|
70
|
+
describe 'Pod4::DatabaseError' do
|
71
|
+
it 'is an exception based on Pod4Error' do
|
72
|
+
expect( Pod4::DatabaseError ).not_to be_nil
|
73
|
+
expect( Pod4::DatabaseError.ancestors ).to include Pod4::Pod4Error
|
74
|
+
end
|
75
|
+
end
|
76
|
+
##
|
77
|
+
|
78
|
+
|
79
|
+
describe 'Pod4::ValidationError' do
|
80
|
+
it 'is an exception based on Pod4Error' do
|
81
|
+
expect( Pod4::ValidationError ).not_to be_nil
|
82
|
+
expect( Pod4::ValidationError.ancestors ).to include Pod4::Pod4Error
|
83
|
+
end
|
84
|
+
end
|
85
|
+
##
|
86
|
+
|
87
|
+
|
88
|
+
end
|