palsy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +91 -0
- data/Rakefile +38 -0
- data/lib/palsy/basic/collection.rb +46 -0
- data/lib/palsy/basic/generic.rb +89 -0
- data/lib/palsy/basic/list.rb +134 -0
- data/lib/palsy/basic/map.rb +92 -0
- data/lib/palsy/basic/object.rb +64 -0
- data/lib/palsy/basic/set.rb +115 -0
- data/lib/palsy/basic.rb +4 -0
- data/lib/palsy/version.rb +1 -0
- data/lib/palsy.rb +92 -0
- data/palsy.gemspec +26 -0
- data/test/helper.rb +51 -0
- data/test/test_collection.rb +18 -0
- data/test/test_generic.rb +35 -0
- data/test/test_init.rb +41 -0
- data/test/test_list.rb +53 -0
- data/test/test_map.rb +78 -0
- data/test/test_object.rb +40 -0
- data/test/test_set.rb +81 -0
- metadata +158 -0
@@ -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
|
data/lib/palsy/basic.rb
ADDED
@@ -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
|
data/test/test_object.rb
ADDED
@@ -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
|