# Tests for *some* of util.rb; this is nowhere near complete. require_relative '../lib/dbmlite3.rb' require 'fileutils' require 'set' def jruby? return RUBY_PLATFORM == "java" end module Tmp @root = File.join( File.dirname(__FILE__), "tmpdata") @count = 0 def self.file FileUtils.mkdir(@root) unless File.directory?(@root) file = "testfile_#{@count}_#{$$}.sqlite3" @count += 1 return File.join(@root, file) end def self.cleanup return unless File.directory?(@root) FileUtils.rm_rf(@root) end end # def enum_finish(enum) # result = [] # result.push enum.next while true # rescue StopIteration => e # return result # end AStruct = Struct.new(:a, :b, :c) Serializations = Set.new [:yaml, :marshal] .each do |enc_arg| enc = enc_arg newdb = proc{ |path, table| Serializations << enc Lite3::DBM.new(path, table, enc) } describe Lite3::DBM do after(:all) do Tmp.cleanup end it "creates files and can reopen them afterward" do path = Tmp.file expect( File.exist?(path) ) .to be false # Create it twice to ensure this is safe db = newdb.call(path, "second") db.close db2 = newdb.call(path, "second") db2.close expect( File.exist?(path) ) .to be true end it "allows closing of DB files" do path = Tmp.file expect( File.exist?(path) ) .to be false # Create it twice to ensure this is safe db = newdb.call(path, "first") db2 = newdb.call(path, "second") expect( db.closed? ) .to be false expect( db2.closed? ) .to be false db.close expect( db.closed? ) .to be true expect( db2.closed? ) .to be false db2.close expect( db2.closed? ) .to be true end it "allows insertion, reading, and replacement of values" do path = Tmp.file expect( File.exist?(path) ) .to be false # Create it twice to ensure this is safe db = newdb.call(path, "first") db["foo"] = 42 expect( db['foo'] ) .to eq 42 db["bar"] = [1,2,3] expect( db['bar'] ) .to eq [1,2,3] db["bar"] = "Hi, there!" expect( db['bar'] ) .to eq "Hi, there!" db.store("bar", "nope") expect( db['bar'] ) .to eq "nope" db.close end it "retrieves the list of keys and values" do path = Tmp.file db = newdb.call(path, "floop") expect( db.keys ) .to eq [] expect( db.values ) .to eq [] db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.keys ) .to eq %w{foo bar quux} expect( db.values ) .to eq [42, 99, 123] db.close end it "preserves order after modification" do path = Tmp.file db = newdb.call(path, "floop") expect( db.keys ) .to eq [] expect( db.values ) .to eq [] db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 db["foo"] = 88 expect( db.keys ) .to eq %w{foo bar quux} expect( db.values ) .to eq [88, 99, 123] db.close end it "implements has_key?" do path = Tmp.file db = newdb.call(path, "floop") expect( db.has_key? :foo ) .to be false db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.has_key? :foo ) .to be true expect( db.include? 'foo' ) .to be true expect( db.has_key? 'bar' ) .to be true expect( db.has_key? :quux ) .to be true expect( db.key? :quux ) .to be true expect( db.include? :quux ) .to be true expect( db.member? :quux ) .to be true expect( db.has_key? '' ) .to be false expect( db.has_key? 'norlllp' ) .to be false expect( db.include? 'norlllp' ) .to be false db.close end it "fails if the key isn't a string or symbol" do path = Tmp.file db = newdb.call(path, "floop") db[:foo] = 42 expect( db['foo'] ) .to eq 42 expect{ db[ [1,2,3] ] = 999 } .to raise_error TypeError db.close end it "implements .clear (deletes all items)" do path = Tmp.file db = newdb.call(path, "floop") db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.keys.sort ) .to eq %w{bar foo quux} expect( db['quux'] ) .to be 123 db.clear expect( db.keys.sort ) .to eq [] expect( db['foo'] ) .to eq nil db.close end it "preserves insertion order" do db = newdb.call(Tmp.file, "floop") db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.keys ) .to eq %w{foo bar quux} db.close end it "implements each_*" do db = newdb.call(Tmp.file, "floop") # Empty DBMs don't evaluate their bloc count = 0 db.each {|k,v| count += 1} db.each_pair {|k,v| count += 1} expect( count ) .to eq 0 db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 pairs = [] db.each_pair{|k,v| pairs.push [k,v]} expect( pairs ) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123] ] keys = [] db.each_key{|k| keys.push k } expect( keys ) .to eq [ "foo", "bar", "quux" ] values = [] db.each_value{|k| values.push k } expect( values ) .to eq [ 42, 99, 123 ] db.close end it "allows database modification in `each`" do db = newdb.call(Tmp.file, "floop") db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 db["baz"] = 999 count = 0 db.each do|key, value| count += 1 case count when 1 expect(key) .to eq "foo" expect(value) .to eq 42 db['foo'] = 'new_foo' when 2 expect(key) .to eq "bar" expect(value) .to eq 99 db['baz'] = "new_baz" expect(db['foo']) .to eq "new_foo" db.delete("quux") when 3 expect(key) .to eq "baz" expect(value) .to eq "new_baz" when 4 fail "there should not be 4 items!" end end expect(count) .to eq 3 db.close end it "implements fast_each" do db = newdb.call(Tmp.file, "floop") db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expected = [] db.fast_each{|key, value| expected.push [key, value]} expect(expected) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123]] db.close end it "deletes items from the table" do db = newdb.call(Tmp.file, "floop") db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.keys ) .to eq %w{foo bar quux} db.delete('bar') expect( db.keys ) .to eq %w{foo quux} expect( db.has_key? 'bar' ) .to be false db.close end it "deletes items from the table if they match a block" do db = newdb.call(Tmp.file, "floop") db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.keys ) .to eq %w{foo bar quux} db.delete_if {|k,v| k == "bar"} expect( db.keys ) .to eq %w{foo quux} expect( db.has_key? 'bar' ) .to be false db.delete_if {|k,v| false } expect( db.keys ) .to eq %w{foo quux} expect( db.has_key? 'bar' ) .to be false db.delete_if {|k,v| v == 123 } expect( db.keys ) .to eq %w{foo} expect( db.has_key? 'quux' ) .to be false db.close end it "returns size, length and tests for emptyness" do db = newdb.call(Tmp.file, "floop") expect( db.empty? ) .to be true db["foo"] = 42 expect( db.empty? ) .to be false expect( db.size ) .to be 1 db["bar"] = 99 expect( db.size ) .to be 2 db["quux"] = 123 expect( db.size ) .to be 3 db.delete("bar") expect( db.size ) .to be 2 expect( db.length ) .to be 2 db.close end it "implements 'fetch'" do db = newdb.call(Tmp.file, "floop") db.store("foo", 42) expect( db.fetch("foo", 999) { |k| 69} ) .to eq 42 expect( db.fetch("foo", 999) ) .to eq 42 expect( db.fetch("foo") ) .to eq 42 expect( db.fetch("foox", 999) { |k| 69} ) .to eq 69 expect( db.fetch("foox", 999) ) .to eq 999 expect{ db.fetch("foox") } .to raise_error IndexError expect{ db.fetch([1,2,3]) } .to raise_error TypeError db.close end it "implements to_a and to_hash" do db = newdb.call(Tmp.file, "floop") expect( db.to_a ) .to eq [] expect( db.to_hash ) .to eq( {} ) db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.to_a ) .to eq [ ['foo', 42], ['bar', 99], ['quux', 123] ] expect( db.to_hash ) .to eq [ ['foo', 42], ['bar', 99], ['quux', 123] ].to_h db.close end it "stores complex data structures" do db = newdb.call(Tmp.file, "floop") thingy = AStruct.new([1, 2, "bar"], :b, {this_field: 42}) db["thingy"] = thingy expect( db["thingy"] ) .to eq thingy db.close end it "implements 'update'." do db = newdb.call(Tmp.file, "floop") expect( db.empty? ) .to be true vv = { foo: 123, bar: "this is some text", quux: [1,2,3], } db.update(vv) vv.each{|k,v| expect( db[k] ) .to eq v } db.close end it "implements nestable 'transaction'" do db = newdb.call(Tmp.file, "floop") db.transaction {|db1| expect( db1 ) .to be db expect( db.to_a ) .to eq [] expect( db.to_hash ) .to eq( {} ) db.transaction {|db2| db2["foo"] = 42 db2["bar"] = 99 } # Test sequences of nested transactions db.transaction {|db2| db2["quux"] = 123 } } expect( db.to_a ) .to eq [ ['foo', 42], ['bar', 99], ['quux', 123] ] # Test a sequence of toplevel transactions db.transaction { db["bobo"] = [1,2,3] } expect( db["bobo"] ) .to eq [1,2,3] db.close end it "rolls back transactions on exception" do db = newdb.call(Tmp.file, "floop") expect do db.transaction { db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 raise "nope" } end .to raise_error RuntimeError expect( db.empty? ) .to be true db.close end it "transaction returns the result of the transaction block" do db = newdb.call(Tmp.file, "floop") result = db.transaction { db['foo'] = 42 } expect( result ) .to be 42 end it "provides the rest of DBM's interface as convenience methods." do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) expect( db.values_at('foo', 'nope', 'quux') ) .to eq vv.values_at('foo', 'nope', 'quux') expect( db.value? 123 ) .to be true expect( db.value? "nice" ) .to be false expect( db.invert ) .to eq vv.invert expect( db.has_value? [1,2,3] ) .to be true expect( db.has_value? "nope") .to be false expect( db.shift ) .to eq vv.shift expect( db.to_hash ) .to eq vv db.close end it "has each* methods return an enumerator if no block is given" do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) e1 = db.each expect( e1.class ) .to be Enumerator expect( e1.next ) .to eq ["foo", 123] expect{ e1.next; e1.next; e1.next } .to raise_error StopIteration e2 = db.each_key expect( e2.class ) .to be Enumerator expect( e2.next ) .to eq "foo" expect( e2.next ) .to eq "bar" expect{ e2.next; e2.next; e2.next } .to raise_error StopIteration e3 = db.each_value expect( e3.class ) .to be Enumerator expect( e3.next ) .to eq 123 expect( e3.next ) .to eq "this is some text" expect{ e3.next; e3.next; e3.next } .to raise_error StopIteration db.close end it "mixes in Enumerable" do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) # Since Enumerable is pretty trusted at this point, we just test # a smattering of methods to ensure nothing is grossly broken expect( db.find{|k,v| k == "quux"} ) .to eq ["quux", vv["quux"]] expect( db.map{|k,v| v} ) .to eq vv.values expect( db.map.each{|k,v| v} ) .to eq vv.values expect( db.first 2 ) .to eq vv.first 2 expect( db.max{|a, b| a[0].size <=> b[0].size} ) .to eq ["quux", [1,2,3]] db.close end it "does select and reject (via Enumerable)" do db = newdb.call(Tmp.file, "floop") expect( db.select{true} ) .to eq [] expect( db.select{false} ) .to eq [] expect( db.reject{true} ) .to eq [] expect( db.reject{false} ) .to eq [] db["foo"] = 42 db["bar"] = 99 db["quux"] = 123 expect( db.select{true} ) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123] ] expect( db.select{false} ) .to eq [] expect( db.reject{false} ) .to eq [ ["foo", 42], ["bar", 99], ["quux", 123] ] expect( db.reject{true} ) .to eq [] expect( db.select{ |k, v| v == 42 || k == "quux" } ) .to eq [ ["foo", 42], ["quux", 123] ] expect( db.reject{ |k, v| v == 42} ) .to eq [["bar", 99], ["quux", 123]] db.close end it "handles database accesses from within an 'each_*' block correctly." do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) ev = nil keys = [] db.each{|k,v| keys.push k ev = db["foo"] if k == "bar" } expect( keys ) .to eq vv.keys expect( ev ) .to eq vv["foo"] ev = nil keys = [] db.each_key{|k| keys.push k ev = db["foo"] if k == "bar" } expect( keys ) .to eq vv.keys expect( ev ) .to eq vv["foo"] ev = nil values = [] db.each_value{|v| values.push v ev = db["foo"] if v != 123 } expect( values ) .to eq vv.values expect( ev ) .to eq vv["foo"] db.close end it "handles puts 'each_*' calls with blocks into their own transaction" do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) expect( db.transaction_active? ) .to be false db.each{ expect( db.transaction_active? ) .to be true } expect( db.transaction_active? ) .to be false db.each_key{ expect( db.transaction_active? ) .to be true } expect( db.transaction_active? ) .to be false db.each_value{ expect( db.transaction_active? ) .to be true } expect( db.transaction_active? ) .to be false db.close end it "does *not* start a transaction for each_* enumerators " do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) for method in %i{each each_key each_value} expect( db.transaction_active? ) .to be false e = db.send(method) expect( db.transaction_active? ) .to be false 3.times { e.next expect( db.transaction_active? ) .to be false } expect{ e.next } .to raise_error StopIteration expect( db.transaction_active? ) .to be false end db.close end it "handles transaction around enumerators correctly" do db = newdb.call(Tmp.file, "blopp") vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], } db.update(vv) [ [:each, vv.to_a], [:each_key, vv.keys ], [:each_value, vv.values ] ].each do | method, exp | e = db.send(method) # First item expect( e.next ) .to eq exp[0] expect( db.transaction_active? ) .to be false # redundant, but... # An 'each' with block in the middle of the sequence doesn't # affect the enumerator tt = [] e.each{|a| tt.push a } expect( tt.size ) .to eq 3 # Ditto for an 'each' on the database tt = [] db.each{|k,v| tt.push [k,v] } expect( tt ) .to eq vv.to_a expect( db.transaction_active? ) .to be false # Second item expect( e.next ) .to eq exp[1] # Ditto for other database reads tmp = db["bar"] expect( e.next ) .to eq exp[2] # Now that we're at the end, reads raise StopIteration expect{ e.next } .to raise_error(StopIteration) expect{ e.next } .to raise_error(StopIteration) # Redundant again, but why not? expect( db.transaction_active? ) .to be false end db.close end end end describe Lite3::DBM do after(:all) do Tmp.cleanup end it "can also encode values using to_s" do db = Lite3::DBM.new(Tmp.file, "tbl", :string) vv = { foo: 123, bar: "this is some text", quux: [1,2,3], } vv.each{|k,v| db[k] = v} expect( db.size ) .to eq 3 vv.each{|k,v| expect( db[k] ) .to eq v.to_s } db.close end it "gets tested against :marshal and :yaml" do # Sanity test to catch if the previous block didn't do both. expect( Serializations ) .to eq [:yaml, :marshal].to_set end it "provides the open-with-block semantics" do db = nil Lite3::DBM.open(Tmp.file, "tbl") { |dbh| db = dbh expect( dbh.closed? ) .to be false dbh["foo"] = 42 expect( dbh["foo"] ) .to eq 42 } expect( db.closed? ) .to be true end it "works with multiple tables in the same database file" do vv = { foo: 123, bar: "this is some text", quux: [1,2,3], } file = Tmp.file dbs = (1..5).to_a.map{|i| Lite3::DBM.new(file, "tbl_#{i}") } vv.each{|k,v| dbs.each{|db| db[k] = v } } vv.each{|k,v| dbs.each{|db| expect( db[k] ) .to eq v } } dbs.each{ |db| db.close} end it "handles multiple instances for the SAME table as well" do vv = { foo: 123, bar: "this is some text", quux: [1,2,3], } file = Tmp.file dbs = (1..3).to_a.map{|i| Lite3::DBM.new(file, "tbl") } vv.to_a.zip(dbs).each{ |pair, db| k, v = pair dbs.each{|db| db[k] = v } } vv.each{|k,v| dbs.each{|db| expect( db[k] ) .to eq v } } dbs.each{ |db| db.close} end it "tests if the underlying SQLite3 lib was compiled to be threadsafe" do expect( [true, false].include? Lite3::SQL.threadsafe? ) .to be true end it "reports on in-progress transactions" do Lite3::DBM.open(Tmp.file, "tbl") { |dbh| expect( dbh.transaction_active? ) .to be false dbh.transaction { expect( dbh.transaction_active? ) .to be true } } end it "detects incorrect serializer mixes" do file = Tmp.file serializers = %i{yaml marshal string} serializers.each do |ser| tbl = "tbl_#{ser}" Lite3::DBM.open(file, tbl, ser) { |db| db["foo"] = "bar" } # Ensure that reloading works Lite3::DBM.open(file, tbl, ser) { |db| expect( db.closed? ) .to be false expect( db["foo"] ) .to eq "bar" } # Ensure that incompatible serializers raise an exception alt = (serializers - [ser])[0] expect { Lite3::DBM.new(file, tbl, alt) } .to raise_error Lite3::Error # Ensure that it's still possible to access the DB afterward Lite3::DBM.open(file, tbl, ser) { |db| expect( db.closed? ) .to be false expect( db["foo"] ) .to eq "bar" db["bobo"] = "1" expect( db["bobo"] ) .to eq "1" } end end it "keeps most of its names private" do expect( Lite3.constants.to_set ) .to eq %i{SQL DBM Error InternalError}.to_set end end describe Lite3::SQL do after(:all) do Tmp.cleanup end vv = { "foo" => 123, "bar" => "this is some text", "quux" => [1,2,3], }.freeze newbasic = proc{ |file, tbl| db = Lite3::DBM.new(file, tbl) vv.each{|k,v| db[k] = v} db } it "lets you close the actual handle without impeding database use" do jruby? or # JRuby GC semantics throw this off expect( Lite3::SQL.gc.size ) .to eq 0 file = Tmp.file db1 = newbasic.call(file, "first") db2 = newbasic.call(file, "second") # The above should be using the same handle, which is currently # open. # # (Depends on GC wierdness so we skip this part for JRuby.) unless jruby? stats = Lite3::SQL.gc expect( stats.keys.size ) .to eq 1 # Referencing DBM objects should be db1 and db2 path, refs = stats.to_a[0] expect( path ) .to eq file expect( refs ) .to eq 2 end # We can no longer test if the underlying file handles are still # open, so we don't. # Test closing it and forcing a re-open Lite3::SQL.close_all expect( db1["foo"] ) .to eq vv["foo"] # Repeat, but this time use the underlying lib Sequel::DATABASES.each(&:disconnect) expect( db1["foo"] ) .to eq vv["foo"] db1.close db2.close jruby? or expect( Lite3::SQL.gc.keys.size ) .to eq 0 end it "allows multiple table accesses in the same transaction" do file = Tmp.file db1 = newbasic.call(file, "first") db2 = Lite3::DBM.new(file, "second") # The big deal is if they're part of the same file db1.transaction { db2.transaction { db2.update(db1) } } vv.each{ |k,v| expect( db2[k] ) .to eq v } # But we should also test it across different files db3 = Lite3::DBM.new(Tmp.file, "third") db2.transaction { db3.transaction { db3.update(db2) } } vv.each{ |k,v| expect( db3[k] ) .to eq v } db1.close db2.close db3.close end it "gracefully catches uses of a closed handle" do file = Tmp.file db1 = newbasic.call(file, "first") db1.close # There are the only methods you can expect to work on a closed # handle expect( db1.closed? ) .to be true expect( db1.to_s.class ) .to be String # Everything else should raise an error expect{ db1["foo"] } .to raise_error Lite3::Error expect{ db1["foo"] = 42 } .to raise_error Lite3::Error expect{ db1.each{} } .to raise_error Lite3::Error expect{ db1.size } .to raise_error Lite3::Error expect{ db1.to_a } .to raise_error Lite3::Error end it "finalizes DBM objects that have gone out of scope." do # This is really difficult to test because there's no reliable way # to get the garbage collector to clean up when we want it to. As # such, we make the attempt and skip with a warning if db2 hasn't # been reclaimed. # # (Dropping into the debugger after GC.start seems to help.) skip "JRuby GC is capricious" if jruby? file = Tmp.file db1 = newbasic.call(file, "first") db2 = Lite3::DBM.new(file, "second") # Two DBMs are currently open. stats = Lite3::SQL.gc expect( stats.size ) .to be 1 expect( stats.values[0] ) .to be 2 # Make db2 a weak reference so it goes out of scope after a GC. # It's possible that a GC redesign will this. db2 = WeakRef.new(db2) GC.start # Uncommenting the next line and then resuming seems to work: #byebug if db2.weakref_alive? db2.close db1.close skip "GC has't reclaimed the handle; bailing." end # There should now be exactly 1 open DBM stats = Lite3::SQL.gc expect( stats.size ) .to be 1 expect( stats.values[0] ) .to be 1 db1.close end end describe self do it "(this test) closes all handles when done with them" do skip "JRuby GC is capricious"if jruby? expect( Lite3::SQL.gc.size ) .to eq 0 end end