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.
- 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
|