palsy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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