palsy 0.0.1

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,115 @@
1
+ require 'palsy/basic/collection'
2
+ require 'set'
3
+
4
+ class Palsy
5
+ #
6
+ # Palsy::Set is an emulation of Ruby's Set class, where items are unordered and unique.
7
+ #
8
+ # Sets depend heavily on Marshal and so does their uniqueness. It's your job
9
+ # to ensure you're dealing with types that are easily marshalled.
10
+ #
11
+ # Palsy::Set is a Palsy::Collection and provides #to_set by way of Ruby's
12
+ # Enumerable and Set libraries.
13
+ #
14
+ # Here's an example:
15
+ #
16
+ # obj = Palsy::Set.new("some_sets", "a_specific_set")
17
+ # obj.add 2
18
+ # obj.include? 2 #=> true
19
+ #
20
+ # This will create a table called "some_sets" and all i/o will be directed to
21
+ # that table with an additional key of "a_specific_set". This allows you to
22
+ # coordinate multiple sets in a single table.
23
+ #
24
+ # Another example:
25
+ #
26
+ # obj1 = Palsy::Set.new("some_sets", "a_specific_set")
27
+ # obj2 = Palsy::Set.new("some_sets", "a_specific_set")
28
+ # obj3 = Palsy::Set.new("some_sets", "a_different_set")
29
+ #
30
+ # obj1 == obj2 #=> true
31
+ # obj1.add 2
32
+ # obj2.include? 2 #=> also true
33
+ # obj3 == obj1 #=> false (different set keys)
34
+ # obj.add 2
35
+ # obj3.add 3
36
+ # obj2.include? 3 #=> also false
37
+ class Set < Collection
38
+
39
+ #
40
+ # Add a value to the set -- if it exists already, it will be replaced to
41
+ # avoid raising a constraint from sqlite.
42
+ #
43
+ def add(key)
44
+ delete(key)
45
+ @db.execute("insert into #{@table_name} (name, key) values (?, ?)", [@object_name, Marshal.dump(key)])
46
+ end
47
+
48
+ #
49
+ # Remove a value from the Set.
50
+ #
51
+ def delete(key)
52
+ @db.execute("delete from #{@table_name} where name=? and key=?", [@object_name, Marshal.dump(key)])
53
+ end
54
+
55
+ #
56
+ # Predicate to determine if a value is in this set.
57
+ #
58
+ def has_key?(key)
59
+ @db.execute("select count(*) from #{@table_name} where name=? and key=?", [@object_name, Marshal.dump(key)]).first.first.to_i > 0
60
+ end
61
+
62
+ alias include? has_key?
63
+
64
+ #
65
+ # Remove all values from the set.
66
+ #
67
+ def clear
68
+ @db.execute("delete from #{@table_name} where name=?", [@object_name])
69
+ end
70
+
71
+ #
72
+ # Replace the existing contents of the set with the contents of another
73
+ # set, or any Enumerable that has a set of unique values.
74
+ #
75
+ def replace(set)
76
+ clear
77
+
78
+ return if set.empty?
79
+
80
+ value_string = ("(?, ?)," * set.count).chop
81
+
82
+ @db.execute("insert into #{@table_name} (name, key) values #{value_string}", set.map { |x| [@object_name, Marshal.dump(x)] }.flatten)
83
+ end
84
+
85
+ #
86
+ # Return a ruby Array of the set. Used for #to_a and #to_set by various Ruby libraries.
87
+ #
88
+ def keys
89
+ @db.execute("select distinct key from #{@table_name} where name=?", [@object_name]).map { |x| Marshal.load(x.first) }
90
+ end
91
+
92
+ #
93
+ # Iterate over the set and yield each item. Modifications made to the values yielded will not be persisted.
94
+ #
95
+ def each
96
+ keys.each { |x| yield x }
97
+ end
98
+
99
+ #
100
+ # Defines the schema for a set.
101
+ #
102
+ def create_table
103
+ @db.execute <<-EOF
104
+ create table if not exists #{@table_name} (
105
+ id integer not null primary key autoincrement,
106
+ name varchar(255) not null,
107
+ key varchar(255) not null,
108
+ UNIQUE(name, key)
109
+ )
110
+ EOF
111
+
112
+ @db.execute "create index if not exists #{@table_name}_name_idx on #{@table_name} (name)"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,4 @@
1
+ require 'palsy/basic/object'
2
+ require 'palsy/basic/list'
3
+ require 'palsy/basic/set'
4
+ require 'palsy/basic/map'
@@ -0,0 +1 @@
1
+ PalsyVersion = "0.0.1"
data/lib/palsy.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'sqlite3'
2
+ require 'singleton'
3
+ require 'delegate'
4
+ require "palsy/version"
5
+
6
+ # Present ruby core data structures in a manner similar to perl's tie backed by a
7
+ # SQLite database. Intended to be as simple as possible, sacrificing performance
8
+ # and flexibility to do so.
9
+ #
10
+ # It is not a 1:1 emulation of tie, as ruby cannot support this. What it does do
11
+ # is provide a convincing enough facsimile by emulating the interface, and making
12
+ # it easy to convert to native ruby types when that's not enough.
13
+ #
14
+ # This library is completely unapologetic with regards to how little it does or
15
+ # how slow it does things.
16
+ #
17
+ # All writes are fully consistent, which is something SQLite gives us. Reads
18
+ # always hit the database. This allows us to reason more clearly about how our
19
+ # data persists, even in concurrent models where shared state can get very
20
+ # complicated to use.
21
+
22
+ class Palsy < DelegateClass(SQLite3::Database)
23
+
24
+ include Singleton
25
+
26
+ # Palsy's version, as a string.
27
+ VERSION = PalsyVersion
28
+
29
+ class << self
30
+ ##
31
+ # The name of the database Palsy will manipulate. Set this before using
32
+ # Palsy. Look at Palsy.change_db for a better way to swap DB's during
33
+ # runtime.
34
+ attr_accessor :database
35
+ end
36
+
37
+ #
38
+ # Given a db name, assigns that to Palsy and initiates a reconnection.
39
+ #
40
+ # Note that existing Palsy objects will immediately start working against
41
+ # this new database, and expect everything needed to read the data to exist,
42
+ # namely the tables they're keyed against.
43
+ #
44
+ def self.change_db(db_name)
45
+ self.database = db_name
46
+ self.reconnect
47
+ end
48
+
49
+ #
50
+ # Initiates a reopening of the SQLite database.
51
+ #
52
+ def self.reconnect
53
+ self.instance.reconnect
54
+ end
55
+
56
+ #
57
+ # Closes the SQLite database.
58
+ #
59
+ def self.close
60
+ self.instance.close
61
+ end
62
+
63
+ #
64
+ # Initializer. Since Palsy is a proper Ruby singleton, calling Palsy.instance
65
+ # will run this the first time, but calling this directly is not advised. Use
66
+ # the Palsy class methods like Palsy.change_db, or set Palsy.database= before
67
+ # working with the rest of the library.
68
+ #
69
+ def initialize
70
+ super(connect)
71
+ end
72
+
73
+ #
74
+ # Initiates a reopening of the SQLite database. Instance method that
75
+ # Palsy.reconnect uses.
76
+ #
77
+ def reconnect
78
+ close rescue nil
79
+ __setobj__(connect)
80
+ end
81
+
82
+ #
83
+ # Opens the database. Note that this is a no-op unless Palsy.database= is set.
84
+ #
85
+ def connect
86
+ if self.class.database
87
+ SQLite3::Database.new(self.class.database)
88
+ end
89
+ end
90
+ end
91
+
92
+ require 'palsy/basic'
data/palsy.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'palsy/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "palsy"
8
+ gem.version = PalsyVersion
9
+ gem.authors = ["Erik Hollensbe"]
10
+ gem.email = ["erik+github@hollensbe.org"]
11
+ gem.description = %q{An extremely simple marshalling persistence layer for SQLite based on perl's tie()}
12
+ gem.summary = %q{An extremely simple marshalling persistence layer for SQLite based on perl's tie()}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'sqlite3', '~> 1.3.6'
21
+
22
+ gem.add_development_dependency 'minitest', '~> 4.5.0'
23
+ gem.add_development_dependency 'rake'
24
+ gem.add_development_dependency 'rdoc', "~> 3.12"
25
+ gem.add_development_dependency 'simplecov'
26
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,51 @@
1
+ require 'minitest/unit'
2
+ require 'tempfile'
3
+ require 'palsy'
4
+
5
+ if ENV["COVERAGE"]
6
+ require 'simplecov'
7
+ SimpleCov.start
8
+ end
9
+
10
+ module Palsy::TestHelper
11
+ def new_dbfile
12
+ @dbfiles ||= [ ]
13
+ dbfile = Tempfile.new('palsy-db')
14
+ @dbfiles << dbfile
15
+ return dbfile
16
+ end
17
+
18
+ def reinitialize
19
+ dbfile = new_dbfile
20
+ Palsy.database = dbfile.path
21
+ Palsy.reconnect
22
+ return dbfile
23
+ end
24
+
25
+ # intended to be run in teardown
26
+ def trash_databases
27
+ Palsy.close rescue nil
28
+
29
+ if @dbfiles
30
+ @dbfiles.each do |dbfile|
31
+ dbfile.unlink rescue nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ class MiniTest::Unit::TestCase
38
+ include Palsy::TestHelper
39
+ end
40
+
41
+ class PalsyTypeTest < MiniTest::Unit::TestCase
42
+ def setup
43
+ Palsy.change_db(new_dbfile.path)
44
+ end
45
+
46
+ def teardown
47
+ trash_databases
48
+ end
49
+ end
50
+
51
+ require 'minitest/autorun'
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ # it has the same requirements Generic does, so here's a shim to walk around
4
+ # that for certain tests.
5
+ class InheritedCollection < Palsy::Collection
6
+ def create_table
7
+ end
8
+ end
9
+
10
+ class TestCollection < PalsyTypeTest
11
+ def test_initialize
12
+ assert_includes(Palsy::Collection.ancestors, Palsy::Generic, 'collections are also generics')
13
+ assert_includes(Palsy::Collection.ancestors, Enumerable, 'collections include enumerable')
14
+ assert_raises(RuntimeError, "an object_name must be provided!") { Palsy::Collection.new('blah', nil) }
15
+ assert_raises(RuntimeError, "Do not use the Generic type directly!") { Palsy::Collection.new('blah', 'barf') }
16
+ assert(InheritedCollection.new('blah', 'foo'))
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ require 'helper'
2
+
3
+ # stub class to assist with testing the generic functionality
4
+ class InheritedGeneric < Palsy::Generic
5
+ def create_table
6
+ end
7
+ end
8
+
9
+ class TestGeneric < PalsyTypeTest
10
+ def test_initialize
11
+ assert_raises(RuntimeError, "a table_name must be provided!") { Palsy::Generic.new(nil) }
12
+ assert_raises(RuntimeError, "Do not use the Generic type directly!") { Palsy::Generic.new('some_table') }
13
+ assert_raises(RuntimeError, "a table_name must be provided!") { InheritedGeneric.new(nil) }
14
+ InheritedGeneric.new('some_table')
15
+ end
16
+
17
+ def test_type_marshalling
18
+ ig = InheritedGeneric.new('some_table')
19
+ assert_equal(Palsy.instance, ig.db, 'database is held by the type')
20
+ duped = Marshal.load(Marshal.dump(ig))
21
+ assert_equal(Palsy.instance, duped.db, 'duped type is still holding the connection')
22
+ end
23
+
24
+ def test_attrs
25
+ ig = InheritedGeneric.new('some_table')
26
+ assert_equal(Palsy.instance, ig.db, 'db is associated with palsy instance')
27
+ assert_equal('some_table', ig.table_name, 'table name is getting set properly')
28
+ refute(ig.object_name, 'object name does not exist if not specified')
29
+
30
+ ig = InheritedGeneric.new('some_other_table', 'some_object')
31
+ assert_equal(Palsy.instance, ig.db, 'db is associated with palsy instance')
32
+ assert_equal('some_other_table', ig.table_name, 'table name is getting set properly')
33
+ assert_equal('some_object', ig.object_name, 'object name is getting set properly')
34
+ end
35
+ end
data/test/test_init.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'helper'
2
+
3
+ class TestInit < MiniTest::Unit::TestCase
4
+ def teardown
5
+ trash_databases
6
+ end
7
+
8
+ def test_connection_roundtrip
9
+ dbfile = new_dbfile
10
+ Palsy.change_db(dbfile.path)
11
+ refute(Palsy.instance.closed?, 'the database is available')
12
+ Palsy.close
13
+ assert(Palsy.instance.closed?, 'the database is closed')
14
+ end
15
+
16
+ def test_reconnect_with_new_db
17
+ # SQLite3::Database has no target/filename attribute, so we detect by
18
+ # writing to the original db, swapping the dbfile, reconnecting and trying
19
+ # to read the value. If we get a value back, we fail.
20
+
21
+ dbfile = new_dbfile
22
+ Palsy.change_db(dbfile.path)
23
+ refute(Palsy.instance.closed?, 'the database is available')
24
+ Palsy.instance.execute "create table foo (bar integer);"
25
+ Palsy.instance.execute "insert into foo (bar) values (1);"
26
+ result = Palsy.instance.execute("select bar from foo").first.first rescue nil
27
+ assert_equal(1, result, 'the value was returned')
28
+
29
+ dbfile = new_dbfile
30
+ Palsy.change_db(dbfile.path)
31
+ refute(Palsy.instance.closed?, 'the second database is available')
32
+ result = Palsy.instance.execute("select bar from foo").first.first rescue nil
33
+ refute_equal(1, result, 'the value was returned')
34
+ end
35
+
36
+ def test_connect_with_no_db
37
+ Palsy.database = nil
38
+ Palsy.reconnect
39
+ assert_raises(NoMethodError) { Palsy.instance.closed? }
40
+ end
41
+ end
data/test/test_list.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'helper'
2
+
3
+ class TestList < PalsyTypeTest
4
+ def setup
5
+ super
6
+ @list = Palsy::List.new('test_list', 'a_list')
7
+ end
8
+
9
+ def test_semantics
10
+ assert_empty(@list.to_a, 'an uninitialized list with no data is empty')
11
+ end
12
+
13
+ def test_mutators
14
+ # this, by side effect tests the ordering code, which is why there aren't
15
+ # any explicit ordering tests.
16
+
17
+ @list.push(1)
18
+ assert_equal([1], @list.to_a, 'pushing appends the list')
19
+
20
+ @list << 2
21
+ assert_equal([1, 2], @list.to_a, 'left shift appends the list')
22
+
23
+ @list.unshift(3)
24
+ assert_equal([3, 1, 2], @list.to_a, 'unshift prepends the list')
25
+
26
+ assert_equal(2, @list.pop, 'pop returns the last value')
27
+ assert_equal([3, 1], @list.to_a, 'pop also removes the last value')
28
+
29
+ assert_equal(3, @list.shift, 'shift returns the first value')
30
+ assert_equal([1], @list.to_a, 'shift also removes the first value')
31
+
32
+ @list.replace([2,3,4])
33
+ assert_equal([2,3,4], @list.to_a, 'replace replaces the contents')
34
+
35
+ @list.clear
36
+ assert_empty(@list.to_a, 'clearing the list makes it empty')
37
+ end
38
+
39
+ def test_iterator
40
+ array = [1,2,3]
41
+ @list.replace(array)
42
+ duped_array = array.dup
43
+ @list.each do |item|
44
+ assert_equal(duped_array.shift, item, 'value in #each is correct')
45
+ end
46
+
47
+ assert_equal(
48
+ [[1], [2, 3]],
49
+ @list.partition { |x| x == 1 },
50
+ 'enumerable methods work as expected'
51
+ )
52
+ end
53
+ end
data/test/test_map.rb ADDED
@@ -0,0 +1,78 @@
1
+ require 'helper'
2
+
3
+ class TestMap < PalsyTypeTest
4
+ def setup
5
+ super
6
+ @map = Palsy::Map.new('test_map', 'a_map')
7
+ @map2 = Palsy::Map.new('test_map', 'a_map')
8
+ end
9
+
10
+ def test_inherits
11
+ assert_includes(Palsy::Map.ancestors, Palsy::Set, 'maps are also sets')
12
+ end
13
+
14
+ def test_mutators
15
+ @map[1] = "foo"
16
+ assert_equal("foo", @map[1], "persistence works")
17
+ assert_equal("foo", @map2[1], "persistence works across multiple objects")
18
+
19
+ @map["stuff"] = "other stuff"
20
+
21
+ assert_equal("other stuff", @map["stuff"], "persistence works (type check)")
22
+ assert_equal("other stuff", @map2["stuff"], "persistence works across multiple objects (type check)")
23
+
24
+ @map["1"] = "bar"
25
+
26
+ assert_equal("bar", @map["1"], "string/integer doesn't collide")
27
+ assert_equal("foo", @map[1], "string/integer doesn't collide")
28
+ assert_equal("bar", @map2["1"], "string/integer doesn't collide (across objects)")
29
+ assert_equal("foo", @map2[1], "string/integer doesn't collide (across objects)")
30
+
31
+ another_map = Palsy::Map.new('test_map', 'another_map')
32
+ another_map["inner"] = "secret"
33
+
34
+ @map["outer"] = another_map
35
+
36
+ assert_equal(another_map, @map["outer"], "maps encapsulate other palsy objects")
37
+ assert_equal(another_map, @map2["outer"], "maps encapsulate other palsy objects (across objects)")
38
+ assert_equal("secret", @map["outer"]["inner"], "nested traversal")
39
+ assert_equal("secret", @map2["outer"]["inner"], "nested traversal (across objects)")
40
+ end
41
+
42
+ def test_to_hash
43
+ @map[1] = "foo"
44
+ @map["stuff"] = "other stuff"
45
+
46
+ assert_equal(
47
+ { 1 => "foo", "stuff" => "other stuff" },
48
+ @map.to_hash,
49
+ "#to_hash includes all the map contents"
50
+ )
51
+
52
+ assert_equal(
53
+ { 1 => "foo", "stuff" => "other stuff" },
54
+ @map2.to_hash,
55
+ "#to_hash includes all the map contents (across objects)"
56
+ )
57
+ end
58
+
59
+ def test_iterators
60
+ @map[1] = "foo"
61
+ @map["stuff"] = "other stuff"
62
+
63
+ keys = [1, "stuff"]
64
+
65
+ refute_empty(@map.keys)
66
+
67
+ @map.keys.each do |k|
68
+ assert_includes(keys, k, "#keys encompasses all the keys in the map")
69
+ end
70
+
71
+ assert_equal(@map.keys.uniq, @map.keys)
72
+
73
+ @map.each do |k, v|
74
+ assert_includes(keys, k, "#each yields each key")
75
+ assert_equal(@map[k], v, "#each yields each value")
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ class TestObject < PalsyTypeTest
4
+ def test_index
5
+ obj = Palsy::Object.new('test_object')
6
+ obj2 = Palsy::Object.new('test_object')
7
+ obj['foo'] = 1
8
+ assert_equal(1, obj2['foo'], 'both objects have atomic shared state')
9
+ obj[2] = 3
10
+ assert_equal(3, obj2[2], 'also works with integer indexes')
11
+ refute(obj['something'], 'returns nil properly on non-existent indexes')
12
+ refute(obj[1], 'returns nil properly on non-existent indexes')
13
+ end
14
+
15
+ def test_marshal
16
+ obj = Palsy::Object.new('test_object')
17
+ obj2 = Palsy::Object.new('test_object')
18
+ obj[0] = [1,2,3]
19
+ assert_equal([1,2,3], obj2[0], 'marshals complex types')
20
+ obj3 = Palsy::Object.new('test_object2')
21
+ obj[1] = obj3
22
+ assert_equal(obj3, obj[1], 'marshals palsy objects')
23
+ assert_equal(obj3, obj2[1], 'marshals palsy objects (with a different object)')
24
+ obj4 = Palsy::Object.new('test_object')
25
+ assert_equal(obj3, obj4[1], 'marshals palsy objects (with a brand new object)')
26
+ end
27
+
28
+ def test_delete
29
+ obj = Palsy::Object.new('test_object')
30
+ obj2 = Palsy::Object.new('test_object')
31
+ obj[0] = 1
32
+ assert_equal(1, obj[0], 'set object exists')
33
+ assert_equal(1, obj2[0], 'set object exists (second object)')
34
+ obj.delete(0)
35
+ refute(obj[0], 'set object no longer exists')
36
+ refute(obj2[0], 'set object no longer exists (other object)')
37
+
38
+ obj.delete('something_that_does_not_exist')
39
+ end
40
+ end
data/test/test_set.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'helper'
2
+
3
+ class TestSet < PalsyTypeTest
4
+ def setup
5
+ super
6
+ @set = Palsy::Set.new('test_set', 'a_set')
7
+ end
8
+
9
+ def test_semantics
10
+ assert_empty(@set.to_set, 'an uninitialized set is empty')
11
+ end
12
+
13
+ def test_mutators
14
+ # this, by side effect tests the ordering code, which is why there aren't
15
+ # any explicit ordering tests.
16
+
17
+ @set.add(1)
18
+ assert_equal(Set.new([1]), @set.to_set, '#add adds to the set')
19
+ @set.add("2")
20
+ assert_equal(Set.new([1, "2"]), @set.to_set, "#add handles types correctly")
21
+
22
+ @set.delete(1)
23
+ refute_equal(Set.new([1, "2"]), @set.to_set, "#delete works")
24
+ assert_equal(Set.new(["2"]), @set.to_set, "#delete works")
25
+
26
+ @set.add(2)
27
+ assert_equal(Set.new([2, "2"]), @set.to_set, "no implicit string/integer casting")
28
+
29
+ @set.delete("2")
30
+ refute_equal(Set.new([2, "2"]), @set.to_set, "#delete works (type check)")
31
+ assert_equal(Set.new([2]), @set.to_set, "#delete works (type check)")
32
+
33
+ @set.replace([3,4,5])
34
+ assert_equal(Set.new([3,4,5]), @set.to_set, "#replace replaces the set")
35
+
36
+ @set.clear
37
+ assert_empty(@set.to_set, '#clear empties the set')
38
+
39
+ another_set = Palsy::Map.new('test_set', 'another_set')
40
+ another_set.add("secret")
41
+
42
+ @set.add(another_set)
43
+ assert_includes(@set, another_set, "sets encapsulate other palsy objects")
44
+
45
+ @set.clear
46
+ end
47
+
48
+ def test_predicates
49
+ @set.add(1)
50
+ @set.add("2")
51
+
52
+ assert(@set.has_key?(1), "has_key? reports inclusion")
53
+ assert(@set.has_key?("2"), "has_key? reports inclusion (type check)")
54
+ refute(@set.has_key?("stuff"), "has_key? reports exclusion")
55
+ refute(@set.has_key?(2), "has_key? reports exclusion (type check)")
56
+ end
57
+
58
+ def test_iterators
59
+ @set.add(1)
60
+ @set.add("2")
61
+ @set.add(2)
62
+
63
+ duped_set = @set.to_set
64
+
65
+ refute_empty(@set.keys)
66
+
67
+ # a nice side effect of the backing db is that we can't actually reliably
68
+ # predict the ordering, and there's nothing keeping it predictable. So,
69
+ # while less obvious, this is how we test #keys.
70
+
71
+ @set.keys.each do |k|
72
+ assert_includes(duped_set, k, '#keys returns all the keys in the set')
73
+ end
74
+
75
+ assert_equal(@set.keys.uniq, @set.keys, "keys are unique")
76
+
77
+ @set.each do |k|
78
+ assert_includes(duped_set, k, '#each yields each key in the set')
79
+ end
80
+ end
81
+ end