dbmlite3 1.0.a1

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.
@@ -0,0 +1,960 @@
1
+
2
+ # Tests for *some* of util.rb; this is nowhere near complete.
3
+
4
+ gem "sqlite3", "~> 1.4"
5
+
6
+ require_relative '../lib/dbmlite3.rb'
7
+ require 'fileutils'
8
+ require 'set'
9
+
10
+ module Tmp
11
+ @root = File.join( File.dirname(__FILE__), "tmpdata")
12
+ @count = 0
13
+
14
+ def self.file
15
+ FileUtils.mkdir(@root) unless File.directory?(@root)
16
+
17
+ file = "testfile_#{@count}_#{$$}.sqlite3"
18
+ @count += 1
19
+
20
+ return File.join(@root, file)
21
+ end
22
+
23
+ def self.cleanup
24
+ return unless File.directory?(@root)
25
+ FileUtils.rm_rf(@root)
26
+ end
27
+ end
28
+
29
+ # def enum_finish(enum)
30
+ # result = []
31
+ # result.push enum.next while true
32
+ # rescue StopIteration => e
33
+ # return result
34
+ # end
35
+
36
+ AStruct = Struct.new(:a, :b, :c)
37
+ Serializations = Set.new
38
+
39
+ [:yaml, :marshal] .each do |enc_arg|
40
+ enc = enc_arg
41
+ newdb = proc{ |path, table|
42
+ Serializations << enc
43
+ Lite3::DBM.new(path, table, enc)
44
+ }
45
+
46
+
47
+
48
+ describe Lite3::DBM do
49
+ after(:all) do
50
+ Tmp.cleanup
51
+ end
52
+
53
+ it "creates files and can reopen them afterward" do
54
+ path = Tmp.file
55
+ expect( File.exist?(path) ) .to be false
56
+
57
+ # Create it twice to ensure this is safe
58
+ db = newdb.call(path, "second")
59
+ db.close
60
+
61
+ db2 = newdb.call(path, "second")
62
+ db2.close
63
+
64
+ expect( File.exist?(path) ) .to be true
65
+ end
66
+
67
+ it "allows closing of DB files" do
68
+ path = Tmp.file
69
+ expect( File.exist?(path) ) .to be false
70
+
71
+ # Create it twice to ensure this is safe
72
+ db = newdb.call(path, "first")
73
+ db2 = newdb.call(path, "second")
74
+
75
+ expect( db.closed? ) .to be false
76
+ expect( db2.closed? ) .to be false
77
+
78
+ db.close
79
+ expect( db.closed? ) .to be true
80
+ expect( db2.closed? ) .to be false
81
+
82
+ db2.close
83
+ expect( db2.closed? ) .to be true
84
+ end
85
+
86
+ it "allows insertion, reading, and replacement of values" do
87
+ path = Tmp.file
88
+ expect( File.exist?(path) ) .to be false
89
+
90
+ # Create it twice to ensure this is safe
91
+ db = newdb.call(path, "first")
92
+
93
+ db["foo"] = 42
94
+ expect( db['foo'] ) .to eq 42
95
+
96
+ db["bar"] = [1,2,3]
97
+ expect( db['bar'] ) .to eq [1,2,3]
98
+
99
+ db["bar"] = "Hi, there!"
100
+ expect( db['bar'] ) .to eq "Hi, there!"
101
+
102
+ db.store("bar", "nope")
103
+ expect( db['bar'] ) .to eq "nope"
104
+
105
+ db.close
106
+ end
107
+
108
+ it "retrieves the list of keys and values" do
109
+ path = Tmp.file
110
+ db = newdb.call(path, "floop")
111
+
112
+ expect( db.keys ) .to eq []
113
+ expect( db.values ) .to eq []
114
+
115
+ db["foo"] = 42
116
+ db["bar"] = 99
117
+ db["quux"] = 123
118
+
119
+ expect( db.keys ) .to eq %w{foo bar quux}
120
+ expect( db.values ) .to eq [42, 99, 123]
121
+
122
+ db.close
123
+ end
124
+
125
+ it "implements has_key?" do
126
+ path = Tmp.file
127
+ db = newdb.call(path, "floop")
128
+
129
+ expect( db.has_key? :foo ) .to be false
130
+
131
+ db["foo"] = 42
132
+ db["bar"] = 99
133
+ db["quux"] = 123
134
+
135
+ expect( db.has_key? :foo ) .to be true
136
+ expect( db.include? 'foo' ) .to be true
137
+ expect( db.has_key? 'bar' ) .to be true
138
+ expect( db.has_key? :quux ) .to be true
139
+
140
+ expect( db.key? :quux ) .to be true
141
+ expect( db.include? :quux ) .to be true
142
+ expect( db.member? :quux ) .to be true
143
+
144
+ expect( db.has_key? '' ) .to be false
145
+ expect( db.has_key? 'norlllp' ) .to be false
146
+ expect( db.include? 'norlllp' ) .to be false
147
+
148
+ db.close
149
+ end
150
+
151
+ it "fails if the key isn't a string or symbol" do
152
+ path = Tmp.file
153
+ db = newdb.call(path, "floop")
154
+
155
+ db[:foo] = 42
156
+ expect( db['foo'] ) .to eq 42
157
+
158
+ expect{ db[ [1,2,3] ] = 999 } .to raise_error TypeError
159
+
160
+ db.close
161
+ end
162
+
163
+ it "implements .clear (deletes all items)" do
164
+ path = Tmp.file
165
+ db = newdb.call(path, "floop")
166
+
167
+ db["foo"] = 42
168
+ db["bar"] = 99
169
+ db["quux"] = 123
170
+
171
+ expect( db.keys.sort ) .to eq %w{bar foo quux}
172
+ expect( db['quux'] ) .to be 123
173
+
174
+ db.clear
175
+
176
+ expect( db.keys.sort ) .to eq []
177
+ expect( db['foo'] ) .to eq nil
178
+
179
+ db.close
180
+ end
181
+
182
+
183
+ it "preserves insertion order" do
184
+ db = newdb.call(Tmp.file, "floop")
185
+
186
+ db["foo"] = 42
187
+ db["bar"] = 99
188
+ db["quux"] = 123
189
+
190
+ expect( db.keys ) .to eq %w{foo bar quux}
191
+
192
+ db.close
193
+ end
194
+
195
+ it "implements each_*" do
196
+ db = newdb.call(Tmp.file, "floop")
197
+
198
+ count = 0
199
+ db.each {|k,v| count += 1}
200
+ db.each_pair {|k,v| count += 1}
201
+
202
+
203
+ db["foo"] = 42
204
+ db["bar"] = 99
205
+ db["quux"] = 123
206
+
207
+ pairs = []
208
+ db.each_pair{|k,v| pairs.push [k,v]}
209
+ expect( pairs ) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123] ]
210
+
211
+ keys = []
212
+ db.each_key{|k| keys.push k }
213
+ expect( keys ) .to eq [ "foo", "bar", "quux" ]
214
+
215
+ values = []
216
+ db.each_value{|k| values.push k }
217
+ expect( values ) .to eq [ 42, 99, 123 ]
218
+
219
+ db.close
220
+ end
221
+
222
+ it "deletes items from the table" do
223
+ db = newdb.call(Tmp.file, "floop")
224
+
225
+ db["foo"] = 42
226
+ db["bar"] = 99
227
+ db["quux"] = 123
228
+ expect( db.keys ) .to eq %w{foo bar quux}
229
+
230
+ db.delete('bar')
231
+ expect( db.keys ) .to eq %w{foo quux}
232
+ expect( db.has_key? 'bar' ) .to be false
233
+
234
+ db.close
235
+ end
236
+
237
+ it "deletes items from the table if they match a block" do
238
+ db = newdb.call(Tmp.file, "floop")
239
+
240
+ db["foo"] = 42
241
+ db["bar"] = 99
242
+ db["quux"] = 123
243
+ expect( db.keys ) .to eq %w{foo bar quux}
244
+
245
+ db.delete_if {|k,v| k == "bar"}
246
+ expect( db.keys ) .to eq %w{foo quux}
247
+ expect( db.has_key? 'bar' ) .to be false
248
+
249
+ db.delete_if {|k,v| false }
250
+ expect( db.keys ) .to eq %w{foo quux}
251
+ expect( db.has_key? 'bar' ) .to be false
252
+
253
+ db.delete_if {|k,v| v == 123 }
254
+ expect( db.keys ) .to eq %w{foo}
255
+ expect( db.has_key? 'quux' ) .to be false
256
+
257
+ db.close
258
+ end
259
+
260
+ it "returns size, length and tests for emptyness" do
261
+ db = newdb.call(Tmp.file, "floop")
262
+
263
+ expect( db.empty? ) .to be true
264
+
265
+ db["foo"] = 42
266
+ expect( db.empty? ) .to be false
267
+ expect( db.size ) .to be 1
268
+
269
+ db["bar"] = 99
270
+ expect( db.size ) .to be 2
271
+
272
+ db["quux"] = 123
273
+ expect( db.size ) .to be 3
274
+
275
+ db.delete("bar")
276
+ expect( db.size ) .to be 2
277
+ expect( db.length ) .to be 2
278
+
279
+ db.close
280
+ end
281
+
282
+ it "implements 'fetch'" do
283
+ db = newdb.call(Tmp.file, "floop")
284
+ db.store("foo", 42)
285
+
286
+ expect( db.fetch("foo", 999) { |k| 69} ) .to eq 42
287
+ expect( db.fetch("foo", 999) ) .to eq 42
288
+ expect( db.fetch("foo") ) .to eq 42
289
+
290
+ expect( db.fetch("foox", 999) { |k| 69} ) .to eq 69
291
+ expect( db.fetch("foox", 999) ) .to eq 999
292
+
293
+ expect{ db.fetch("foox") } .to raise_error IndexError
294
+
295
+ expect{ db.fetch([1,2,3]) } .to raise_error TypeError
296
+
297
+ db.close
298
+ end
299
+
300
+ it "implements to_a and to_hash" do
301
+ db = newdb.call(Tmp.file, "floop")
302
+
303
+ expect( db.to_a ) .to eq []
304
+ expect( db.to_hash ) .to eq( {} )
305
+
306
+ db["foo"] = 42
307
+ db["bar"] = 99
308
+ db["quux"] = 123
309
+
310
+ expect( db.to_a ) .to eq [ ['foo', 42], ['bar', 99], ['quux', 123] ]
311
+ expect( db.to_hash )
312
+ .to eq [ ['foo', 42], ['bar', 99], ['quux', 123] ].to_h
313
+
314
+ db.close
315
+ end
316
+
317
+ it "stores complex data structures" do
318
+ db = newdb.call(Tmp.file, "floop")
319
+
320
+ thingy = AStruct.new([1, 2, "bar"], :b, {this_field: 42})
321
+ db["thingy"] = thingy
322
+ expect( db["thingy"] ) .to eq thingy
323
+
324
+ db.close
325
+ end
326
+
327
+ it "implements 'update'." do
328
+ db = newdb.call(Tmp.file, "floop")
329
+ expect( db.empty? ) .to be true
330
+
331
+ vv = {
332
+ foo: 123,
333
+ bar: "this is some text",
334
+ quux: [1,2,3],
335
+ }
336
+
337
+ db.update(vv)
338
+
339
+ vv.each{|k,v| expect( db[k] ) .to eq v }
340
+
341
+ db.close
342
+ end
343
+
344
+ it "implements nestable 'transaction'" do
345
+ db = newdb.call(Tmp.file, "floop")
346
+
347
+ db.transaction {|db1|
348
+ expect( db1 ) .to be db
349
+
350
+ expect( db.to_a ) .to eq []
351
+ expect( db.to_hash ) .to eq( {} )
352
+
353
+ db.transaction {|db2|
354
+ db2["foo"] = 42
355
+ db2["bar"] = 99
356
+ }
357
+
358
+ # Test sequences of nested transactions
359
+ db.transaction {|db2|
360
+ db2["quux"] = 123
361
+ }
362
+ }
363
+
364
+ expect( db.to_a ) .to eq [ ['foo', 42], ['bar', 99], ['quux', 123] ]
365
+
366
+ # Test a sequence of toplevel transactions
367
+ db.transaction {
368
+ db["bobo"] = [1,2,3]
369
+ }
370
+
371
+ expect( db["bobo"] ) .to eq [1,2,3]
372
+
373
+ db.close
374
+ end
375
+
376
+ it "rolls back transactions on exception" do
377
+ db = newdb.call(Tmp.file, "floop")
378
+
379
+ expect do
380
+ db.transaction {
381
+ db["foo"] = 42
382
+ db["bar"] = 99
383
+ db["quux"] = 123
384
+
385
+ raise "nope"
386
+ }
387
+ end .to raise_error RuntimeError
388
+
389
+ expect( db.empty? ) .to be true
390
+
391
+ db.close
392
+ end
393
+
394
+ it "provides the rest of DBM's interface as convenience methods." do
395
+ db = newdb.call(Tmp.file, "blopp")
396
+ vv = {
397
+ "foo" => 123,
398
+ "bar" => "this is some text",
399
+ "quux" => [1,2,3],
400
+ }
401
+
402
+ db.update(vv)
403
+
404
+ expect( db.values_at('foo', 'nope', 'quux') )
405
+ .to eq vv.values_at('foo', 'nope', 'quux')
406
+
407
+ expect( db.value? 123 ) .to be true
408
+ expect( db.value? "nice" ) .to be false
409
+
410
+ expect( db.invert ) .to eq vv.invert
411
+
412
+ expect( db.has_value? [1,2,3] ) .to be true
413
+ expect( db.has_value? "nope") .to be false
414
+
415
+ expect( db.shift ) .to eq vv.shift
416
+ expect( db.to_hash ) .to eq vv
417
+
418
+ db.close
419
+ end
420
+
421
+ it "has each* methods return an enumerator if no block is given" do
422
+ db = newdb.call(Tmp.file, "blopp")
423
+ vv = {
424
+ "foo" => 123,
425
+ "bar" => "this is some text",
426
+ "quux" => [1,2,3],
427
+ }
428
+ db.update(vv)
429
+
430
+ e1 = db.each
431
+ expect( e1.class ) .to be Enumerator
432
+ expect( e1.next ) .to eq ["foo", 123]
433
+ expect{ e1.next; e1.next; e1.next } .to raise_error StopIteration
434
+
435
+ e2 = db.each_key
436
+ expect( e2.class ) .to be Enumerator
437
+ expect( e2.next ) .to eq "foo"
438
+ expect( e2.next ) .to eq "bar"
439
+ expect{ e2.next; e2.next; e2.next } .to raise_error StopIteration
440
+
441
+ e3 = db.each_value
442
+ expect( e3.class ) .to be Enumerator
443
+ expect( e3.next ) .to eq 123
444
+ expect( e3.next ) .to eq "this is some text"
445
+ expect{ e3.next; e3.next; e3.next } .to raise_error StopIteration
446
+
447
+ db.close
448
+ end
449
+
450
+ it "mixes in Enumerable" do
451
+ db = newdb.call(Tmp.file, "blopp")
452
+ vv = {
453
+ "foo" => 123,
454
+ "bar" => "this is some text",
455
+ "quux" => [1,2,3],
456
+ }
457
+ db.update(vv)
458
+
459
+ # Since Enumerable is pretty trusted at this point, we just test
460
+ # a smattering of methods to ensure nothing is grossly broken
461
+
462
+ expect( db.find{|k,v| k == "quux"} ) .to eq ["quux", vv["quux"]]
463
+
464
+ expect( db.map{|k,v| v} ) .to eq vv.values
465
+ expect( db.map.each{|k,v| v} ) .to eq vv.values
466
+
467
+ expect( db.first 2 ) .to eq vv.first 2
468
+
469
+ expect( db.max{|a, b| a[0].size <=> b[0].size} ) .to eq ["quux", [1,2,3]]
470
+
471
+ db.close
472
+ end
473
+
474
+ it "does select and reject (via Enumerable)" do
475
+ db = newdb.call(Tmp.file, "floop")
476
+
477
+ expect( db.select{true} ) .to eq []
478
+ expect( db.select{false} ) .to eq []
479
+ expect( db.reject{true} ) .to eq []
480
+ expect( db.reject{false} ) .to eq []
481
+
482
+ db["foo"] = 42
483
+ db["bar"] = 99
484
+ db["quux"] = 123
485
+
486
+ expect( db.select{true} ) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123] ]
487
+ expect( db.select{false} ) .to eq []
488
+ expect( db.reject{false} ) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123] ]
489
+ expect( db.reject{true} ) .to eq []
490
+
491
+ expect( db.select{ |k, v| v == 42 || k == "quux" } )
492
+ .to eq [ ["foo", 42], ["quux", 123] ]
493
+
494
+ expect( db.reject{ |k, v| v == 42} ) .to eq [["bar", 99], ["quux", 123]]
495
+
496
+ db.close
497
+ end
498
+
499
+ it "handles database accesses from within an 'each_*' block correctly." do
500
+ db = newdb.call(Tmp.file, "blopp")
501
+ vv = {
502
+ "foo" => 123,
503
+ "bar" => "this is some text",
504
+ "quux" => [1,2,3],
505
+ }
506
+ db.update(vv)
507
+
508
+ ev = nil
509
+ keys = []
510
+ db.each{|k,v|
511
+ keys.push k
512
+ ev = db["foo"] if k == "bar"
513
+ }
514
+ expect( keys ) .to eq vv.keys
515
+ expect( ev ) .to eq vv["foo"]
516
+
517
+ ev = nil
518
+ keys = []
519
+ db.each_key{|k|
520
+ keys.push k
521
+ ev = db["foo"] if k == "bar"
522
+ }
523
+ expect( keys ) .to eq vv.keys
524
+ expect( ev ) .to eq vv["foo"]
525
+
526
+ ev = nil
527
+ values = []
528
+ db.each_value{|v|
529
+ values.push v
530
+ ev = db["foo"] if v != 123
531
+ }
532
+ expect( values ) .to eq vv.values
533
+ expect( ev ) .to eq vv["foo"]
534
+
535
+ db.close
536
+ end
537
+
538
+
539
+ it "handles puts 'each_*' calls with blocks into their own transaction" do
540
+ db = newdb.call(Tmp.file, "blopp")
541
+ vv = {
542
+ "foo" => 123,
543
+ "bar" => "this is some text",
544
+ "quux" => [1,2,3],
545
+ }
546
+ db.update(vv)
547
+
548
+ expect( db.transaction_active? ) .to be false
549
+ db.each{
550
+ expect( db.transaction_active? ) .to be true
551
+ }
552
+ expect( db.transaction_active? ) .to be false
553
+ db.each_key{
554
+ expect( db.transaction_active? ) .to be true
555
+ }
556
+ expect( db.transaction_active? ) .to be false
557
+ db.each_value{
558
+ expect( db.transaction_active? ) .to be true
559
+ }
560
+ expect( db.transaction_active? ) .to be false
561
+
562
+ db.close
563
+ end
564
+
565
+ it "does *not* start a transaction for each_* enumerators " do
566
+ db = newdb.call(Tmp.file, "blopp")
567
+ vv = {
568
+ "foo" => 123,
569
+ "bar" => "this is some text",
570
+ "quux" => [1,2,3],
571
+ }
572
+ db.update(vv)
573
+
574
+ for method in %i{each each_key each_value}
575
+ expect( db.transaction_active? ) .to be false
576
+
577
+ e = db.send(method)
578
+ expect( db.transaction_active? ) .to be false
579
+
580
+ 3.times {
581
+ e.next
582
+ expect( db.transaction_active? ) .to be false
583
+ }
584
+
585
+ expect{ e.next } .to raise_error StopIteration
586
+ expect( db.transaction_active? ) .to be false
587
+ end
588
+
589
+ db.close
590
+ end
591
+
592
+ it "handles transaction around enumerators correctly" do
593
+ db = newdb.call(Tmp.file, "blopp")
594
+ vv = {
595
+ "foo" => 123,
596
+ "bar" => "this is some text",
597
+ "quux" => [1,2,3],
598
+ }
599
+ db.update(vv)
600
+
601
+ [
602
+ [:each, vv.to_a],
603
+ [:each_key, vv.keys ],
604
+ [:each_value, vv.values ]
605
+ ].each do | method, exp |
606
+ e = db.send(method)
607
+
608
+ # First item
609
+ expect( e.next ) .to eq exp[0]
610
+ expect( db.transaction_active? ) .to be false # redundant, but...
611
+
612
+ # An 'each' with block in the middle of the sequence doesn't
613
+ # affect the enumerator
614
+ tt = []
615
+ e.each{|a| tt.push a }
616
+ expect( tt.size ) .to eq 3
617
+
618
+ # Ditto for an 'each' on the database
619
+ tt = []
620
+ db.each{|k,v| tt.push [k,v] }
621
+ expect( tt ) .to eq vv.to_a
622
+ expect( db.transaction_active? ) .to be false
623
+
624
+ # Second item
625
+ expect( e.next ) .to eq exp[1]
626
+
627
+ # Ditto for other database reads
628
+ tmp = db["bar"]
629
+ expect( e.next ) .to eq exp[2]
630
+
631
+ # Now that we're at the end, reads raise StopIteration
632
+ expect{ e.next } .to raise_error(StopIteration)
633
+ expect{ e.next } .to raise_error(StopIteration)
634
+
635
+ # Redundant again, but why not?
636
+ expect( db.transaction_active? ) .to be false
637
+ end
638
+
639
+ db.close
640
+ end
641
+
642
+ end
643
+ end
644
+
645
+
646
+ describe Lite3::DBM do
647
+ after(:all) do
648
+ Tmp.cleanup
649
+ end
650
+
651
+ it "can also encode values using to_s" do
652
+ db = Lite3::DBM.new(Tmp.file, "tbl", :string)
653
+ vv = {
654
+ foo: 123,
655
+ bar: "this is some text",
656
+ quux: [1,2,3],
657
+ }
658
+
659
+ vv.each{|k,v| db[k] = v}
660
+
661
+ expect( db.size ) .to eq 3
662
+
663
+ vv.each{|k,v| expect( db[k] ) .to eq v.to_s }
664
+
665
+ db.close
666
+ end
667
+
668
+ it "gets tested against :marshal and :yaml" do
669
+ # Sanity test to catch if the previous block didn't do both.
670
+ expect( Serializations ) .to eq [:yaml, :marshal].to_set
671
+ end
672
+
673
+ it "provides the open-with-block semantics" do
674
+ db = nil
675
+ Lite3::DBM.open(Tmp.file, "tbl") { |dbh|
676
+ db = dbh
677
+ expect( dbh.closed? ) .to be false
678
+
679
+ dbh["foo"] = 42
680
+ expect( dbh["foo"] ) .to eq 42
681
+ }
682
+
683
+ expect( db.closed? ) .to be true
684
+ end
685
+
686
+ it "works with multiple tables in the same database file" do
687
+ vv = {
688
+ foo: 123,
689
+ bar: "this is some text",
690
+ quux: [1,2,3],
691
+ }
692
+
693
+ file = Tmp.file
694
+ dbs = (1..5).to_a.map{|i| Lite3::DBM.new(file, "tbl_#{i}") }
695
+
696
+ vv.each{|k,v|
697
+ dbs.each{|db| db[k] = v }
698
+ }
699
+
700
+ vv.each{|k,v|
701
+ dbs.each{|db|
702
+ expect( db[k] ) .to eq v
703
+ }
704
+ }
705
+
706
+ dbs.each{ |db| db.close}
707
+ end
708
+
709
+ it "handles multiple instances for the SAME table as well" do
710
+ vv = {
711
+ foo: 123,
712
+ bar: "this is some text",
713
+ quux: [1,2,3],
714
+ }
715
+
716
+ file = Tmp.file
717
+ dbs = (1..3).to_a.map{|i| Lite3::DBM.new(file, "tbl") }
718
+
719
+ vv.to_a.zip(dbs).each{ |pair, db|
720
+ k, v = pair
721
+ dbs.each{|db| db[k] = v }
722
+ }
723
+
724
+ vv.each{|k,v|
725
+ dbs.each{|db|
726
+ expect( db[k] ) .to eq v
727
+ }
728
+ }
729
+
730
+ dbs.each{ |db| db.close}
731
+ end
732
+
733
+ it "tests if the underlying SQLite3 lib was compiled to be threadsafe" do
734
+ expect( [true, false].include? Lite3::SQL.threadsafe? ) .to be true
735
+ end
736
+
737
+ it "reports on in-progress transactions" do
738
+ Lite3::DBM.open(Tmp.file, "tbl") { |dbh|
739
+ expect( dbh.transaction_active? ) .to be false
740
+ dbh.transaction {
741
+ expect( dbh.transaction_active? ) .to be true
742
+ }
743
+ }
744
+ end
745
+
746
+ it "detects incorrect serializer mixes" do
747
+ file = Tmp.file
748
+ serializers = %i{yaml marshal string}
749
+
750
+ serializers.each do |ser|
751
+ tbl = "tbl_#{ser}"
752
+ Lite3::DBM.open(file, tbl, ser) { |db|
753
+ db["foo"] = "bar"
754
+ }
755
+
756
+ # Ensure that reloading works
757
+ Lite3::DBM.open(file, tbl, ser) { |db|
758
+ expect( db.closed? ) .to be false
759
+ expect( db["foo"] ) .to eq "bar"
760
+ }
761
+
762
+ # Ensure that incompatible serializers raise an exception
763
+ alt = (serializers - [ser])[0]
764
+ expect { Lite3::DBM.new(file, tbl, alt) } .to raise_error Lite3::Error
765
+
766
+ # Ensure that it's still possible to access the DB afterward
767
+ Lite3::DBM.open(file, tbl, ser) { |db|
768
+ expect( db.closed? ) .to be false
769
+ expect( db["foo"] ) .to eq "bar"
770
+
771
+ db["bobo"] = "1"
772
+ expect( db["bobo"] ) .to eq "1"
773
+ }
774
+ end
775
+ end
776
+
777
+ it "keeps most of its names private" do
778
+ expect( Lite3.constants.to_set ) .to eq %i{SQL DBM Error}.to_set
779
+ end
780
+ end
781
+
782
+
783
+
784
+
785
+ describe Lite3::SQL do
786
+ after(:all) do
787
+ Tmp.cleanup
788
+ end
789
+
790
+ vv = {
791
+ "foo" => 123,
792
+ "bar" => "this is some text",
793
+ "quux" => [1,2,3],
794
+ }.freeze
795
+
796
+ newbasic = proc{ |file, tbl|
797
+ db = Lite3::DBM.new(file, tbl)
798
+ vv.each{|k,v| db[k] = v}
799
+ db
800
+ }
801
+
802
+ # it "manages a pool of DB handles that should now all be closed." do
803
+ # # If this fails, it (probably) means the previous tests didn't
804
+ # # clean up after themselves.
805
+ # GC.start
806
+ # expect( Lite3::SQL.gc.empty? ) .to be true
807
+
808
+ # Lite3::SQL.close_all # smoketest
809
+ # end
810
+
811
+ it "lets you close the actual handle without impeding database use" do
812
+ expect( Lite3::SQL.gc.size ) .to eq 0
813
+
814
+ file = Tmp.file
815
+ db1 = newbasic.call(file, "first")
816
+ db2 = newbasic.call(file, "second")
817
+
818
+ # The above should be using the same handle, which is currently
819
+ # open.
820
+
821
+ stats = Lite3::SQL.gc
822
+ expect( stats.keys.size ) .to eq 1
823
+
824
+ # Referencing DBM objects should be db1 and db2
825
+ path, refs = stats.to_a[0]
826
+
827
+ expect( refs.size ) .to eq 2
828
+ expect( refs.include?(db1) ) .to be true
829
+ expect( refs.include?(db2) ) .to be true
830
+
831
+ # Underlying handles should be open
832
+ expect( db1.handle_closed? ) .to be false
833
+ expect( db2.handle_closed? ) .to be false
834
+
835
+ # Test closing it
836
+ Lite3::SQL.close_all
837
+ expect( db1.handle_closed? ) .to be true
838
+ expect( db2.handle_closed? ) .to be true
839
+
840
+ # Test auto-opening them.
841
+ expect( db1["foo"] ) .to eq vv["foo"]
842
+ expect( db1.handle_closed? ) .to be false
843
+ expect( db2.handle_closed? ) .to be false
844
+
845
+ db1.close
846
+ db2.close
847
+
848
+ expect( Lite3::SQL.gc.keys.size ) .to eq 0
849
+ end
850
+
851
+ # it "(eventually) closes handles that have gone out of scope" do
852
+ # expect( Lite3::SQL.gc.keys.size ) .to eq 0
853
+
854
+ # file = Tmp.file
855
+ # db1 = newbasic.call(file, "first")
856
+
857
+ # expect( db1.handle_closed? ) .to be false
858
+ # expect( Lite3::SQL.gc.keys.size ) .to eq 1
859
+
860
+ # db1 = nil
861
+ # GC.start
862
+ # expect( Lite3::SQL.gc.keys.size ) .to eq 0
863
+ # end
864
+
865
+ it "does close_all with multiple files" do
866
+ db1 = newbasic.call(Tmp.file, "first")
867
+ db2 = newbasic.call(Tmp.file, "second")
868
+
869
+ # The above should be using the same handle, which is currently
870
+ # open.
871
+
872
+ stats = Lite3::SQL.gc
873
+ expect( stats.keys.size ) .to eq 2
874
+
875
+ all_refs = stats.values.flatten
876
+ expect( all_refs.include?(db1) ) .to be true
877
+ expect( all_refs.include?(db2) ) .to be true
878
+
879
+ # Underlying handles should be open
880
+ expect( db1.handle_closed? ) .to be false
881
+ expect( db2.handle_closed? ) .to be false
882
+
883
+ # Test closing it
884
+ Lite3::SQL.close_all
885
+ expect( db1.handle_closed? ) .to be true
886
+ expect( db2.handle_closed? ) .to be true
887
+
888
+ # Test auto-opening them.
889
+ expect( db1["foo"] ) .to eq vv["foo"]
890
+ expect( db1.handle_closed? ) .to be false
891
+ expect( db2.handle_closed? ) .to be true
892
+
893
+ db1.close
894
+ db2.close
895
+
896
+ expect( Lite3::SQL.gc.keys.size ) .to eq 0
897
+ end
898
+
899
+
900
+ it "allows multipe table accesses in the same transaction" do
901
+ file = Tmp.file
902
+ db1 = newbasic.call(file, "first")
903
+ db2 = Lite3::DBM.new(file, "second")
904
+
905
+ # The big deal is if they're part of the same file
906
+ db1.transaction {
907
+ db2.transaction {
908
+ db2.update(db1)
909
+ }
910
+ }
911
+
912
+ vv.each{ |k,v|
913
+ expect( db2[k] ) .to eq v
914
+ }
915
+
916
+ # But we should also test it across different files
917
+ db3 = Lite3::DBM.new(Tmp.file, "third")
918
+ db2.transaction {
919
+ db3.transaction {
920
+ db3.update(db2)
921
+ }
922
+ }
923
+
924
+ vv.each{ |k,v|
925
+ expect( db3[k] ) .to eq v
926
+ }
927
+
928
+ db1.close
929
+ db2.close
930
+ db3.close
931
+ end
932
+
933
+ it "gracefully catches uses of a closed handle" do
934
+ file = Tmp.file
935
+ db1 = newbasic.call(file, "first")
936
+ db1.close
937
+
938
+ # There are the only methods you can expect to work on a closed
939
+ # handle
940
+ expect( db1.closed? ) .to be true
941
+ expect( db1.to_s.class ) .to be String
942
+
943
+ # Everything else shoudl raise an error
944
+ expect{ db1["foo"] } .to raise_error Lite3::Error
945
+ expect{ db1["foo"] = 42 } .to raise_error Lite3::Error
946
+ expect{ db1.each{} } .to raise_error Lite3::Error
947
+ expect{ db1.size } .to raise_error Lite3::Error
948
+ expect{ db1.to_a } .to raise_error Lite3::Error
949
+
950
+ # Ensure we haven't accidentally overridded superclass methods.
951
+ expect( db1.object_id.class ) .to be Integer
952
+ end
953
+ end
954
+
955
+
956
+ describe self do
957
+ it "(this test) closes all handles when done with them" do
958
+ expect( Lite3::SQL.gc.size ) .to eq 0
959
+ end
960
+ end