perobs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +113 -0
- data/Rakefile +22 -0
- data/lib/perobs/Array.rb +173 -0
- data/lib/perobs/BlockDB.rb +242 -0
- data/lib/perobs/Cache.rb +201 -0
- data/lib/perobs/DataBase.rb +115 -0
- data/lib/perobs/FileSystemDB.rb +171 -0
- data/lib/perobs/Hash.rb +175 -0
- data/lib/perobs/HashedBlocksDB.rb +153 -0
- data/lib/perobs/Object.rb +189 -0
- data/lib/perobs/ObjectBase.rb +159 -0
- data/lib/perobs/Store.rb +290 -0
- data/lib/perobs/version.rb +4 -0
- data/lib/perobs.rb +29 -0
- data/perobs.gemspec +23 -0
- data/spec/Array_spec.rb +94 -0
- data/spec/FileSystemDB_spec.rb +107 -0
- data/spec/Hash_spec.rb +96 -0
- data/spec/Object_spec.rb +108 -0
- data/spec/Store_spec.rb +412 -0
- data/spec/perobs_spec.rb +155 -0
- data/tasks/changelog.rake +169 -0
- data/tasks/gem.rake +50 -0
- data/tasks/rdoc.rake +14 -0
- data/tasks/test.rake +7 -0
- metadata +121 -0
data/lib/perobs/Store.rb
ADDED
@@ -0,0 +1,290 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = Store.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require 'perobs/Cache'
|
29
|
+
require 'perobs/FileSystemDB'
|
30
|
+
require 'perobs/HashedBlocksDB'
|
31
|
+
require 'perobs/Object'
|
32
|
+
require 'perobs/Hash'
|
33
|
+
require 'perobs/Array'
|
34
|
+
|
35
|
+
# PErsistent Ruby OBject Store
|
36
|
+
module PEROBS
|
37
|
+
|
38
|
+
# PEROBS::Store is a persistent storage system for Ruby objects. Regular
|
39
|
+
# Ruby objects are transparently stored in a back-end storage and retrieved
|
40
|
+
# when needed. It features a garbage collector that removes all objects that
|
41
|
+
# are no longer in use. A build-in cache keeps access latencies to recently
|
42
|
+
# used objects low and lazily flushes modified objects into the persistend
|
43
|
+
# back-end. Currently only filesystems are supported but it can easily be
|
44
|
+
# extended to any key/value database.
|
45
|
+
#
|
46
|
+
# Persistent objects must be created by deriving your class from
|
47
|
+
# PEROBS::Object. Only instance variables that are declared via
|
48
|
+
# po_attr will be persistent. It is recommended that references to other
|
49
|
+
# objects are all going to persistent objects again.
|
50
|
+
class Store
|
51
|
+
|
52
|
+
attr_reader :db, :cache
|
53
|
+
|
54
|
+
# Create a new Store.
|
55
|
+
# @param data_base [String] the name of the database
|
56
|
+
# @param options [Hash] various options to affect the operation of the
|
57
|
+
# database. Currently the following options are supported:
|
58
|
+
# :engine : The class that provides the back-end storage
|
59
|
+
# engine. By default HashedBlocksDB is used. A user
|
60
|
+
# can provide it's own storage engine that must
|
61
|
+
# conform to the same API exposed by HashedBlocksDB.
|
62
|
+
# :cache_bits : the number of bits used for cache indexing. The
|
63
|
+
# cache will hold 2 to the power of bits number of
|
64
|
+
# objects. We have separate caches for reading and
|
65
|
+
# writing. The default value is 16. It probably makes
|
66
|
+
# little sense to use much larger numbers than that.
|
67
|
+
# :serializer : select the format used to serialize the data. There
|
68
|
+
# are 3 different options:
|
69
|
+
# :marshal : Native Ruby serializer. Fastest option
|
70
|
+
# that can handle most Ruby data types. Big
|
71
|
+
# disadvantate is the stability of the format. Data
|
72
|
+
# written with one Ruby version may not be readable
|
73
|
+
# with another version.
|
74
|
+
# :json : About half as fast as marshal, but the
|
75
|
+
# format is rock solid and portable between
|
76
|
+
# languages. It only supports basic Ruby data types
|
77
|
+
# like String, Fixnum, Float, Array, Hash. This is
|
78
|
+
# the default option.
|
79
|
+
# :yaml : Can also handle most Ruby data types and is
|
80
|
+
# portable between Ruby versions (1.9 and later).
|
81
|
+
# Unfortunately, it is 10x slower than marshal.
|
82
|
+
def initialize(data_base, options = {})
|
83
|
+
# Create a backing store handler
|
84
|
+
@db = (options[:engine] || HashedBlocksDB).new(data_base, options)
|
85
|
+
|
86
|
+
# The Cache reduces read and write latencies by keeping a subset of the
|
87
|
+
# objects in memory.
|
88
|
+
@cache = Cache.new(options[:cache_bits] || 16)
|
89
|
+
|
90
|
+
# The named (global) objects IDs hashed by their name
|
91
|
+
unless (@root_objects = object_by_id(0))
|
92
|
+
@root_objects = Hash.new(self)
|
93
|
+
# The root object hash always has the object ID 0.
|
94
|
+
@root_objects._change_id(0)
|
95
|
+
# The ID change removes it from the write cache. We need to add it
|
96
|
+
# again.
|
97
|
+
@cache.cache_write(@root_objects)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Store the provided object under the given name. Use this to make the
|
102
|
+
# object a root or top-level object (think global variable). Each store
|
103
|
+
# should have at least one root object. Objects that are not directly or
|
104
|
+
# indirectly reachable via any of the root objects are no longer
|
105
|
+
# accessible and will be garbage collected.
|
106
|
+
# @param name [Symbol] The name to use.
|
107
|
+
# @param obj [PEROBS::Object] The object to store
|
108
|
+
# @return [PEROBS::Object] The stored object.
|
109
|
+
def []=(name, obj)
|
110
|
+
# If the passed object is nil, we delete the entry if it exists.
|
111
|
+
if obj.nil?
|
112
|
+
@root_objects.delete(name)
|
113
|
+
return nil
|
114
|
+
end
|
115
|
+
|
116
|
+
# We only allow derivatives of PEROBS::Object to be stored in the
|
117
|
+
# store.
|
118
|
+
unless obj.is_a?(ObjectBase)
|
119
|
+
raise ArgumentError, "Object must be of class PEROBS::Object but "
|
120
|
+
"is of class #{obj.class}"
|
121
|
+
end
|
122
|
+
|
123
|
+
unless obj.store == self
|
124
|
+
raise ArgumentError, 'The object does not belong to this store.'
|
125
|
+
end
|
126
|
+
|
127
|
+
# Store the name and mark the name list as modified.
|
128
|
+
@root_objects[name] = obj._id
|
129
|
+
# Add the object to the in-memory storage list.
|
130
|
+
@cache.cache_write(obj)
|
131
|
+
|
132
|
+
obj
|
133
|
+
end
|
134
|
+
|
135
|
+
# Return the object with the provided name.
|
136
|
+
# @param name [Symbol] A Symbol specifies the name of the object to be
|
137
|
+
# returned.
|
138
|
+
# @return The requested object or nil if it doesn't exist.
|
139
|
+
def [](name)
|
140
|
+
# Return nil if there is no object with that name.
|
141
|
+
return nil unless (id = @root_objects[name])
|
142
|
+
|
143
|
+
object_by_id(id)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Flush out all modified objects to disk and shrink the in-memory list if
|
147
|
+
# needed.
|
148
|
+
def sync
|
149
|
+
if @cache.in_transaction?
|
150
|
+
raise RuntimeError, 'You cannot call sync during a transaction'
|
151
|
+
end
|
152
|
+
@cache.flush
|
153
|
+
end
|
154
|
+
|
155
|
+
# Discard all objects that are not somehow connected to the root objects
|
156
|
+
# from the back-end storage. The garbage collector is not invoked
|
157
|
+
# automatically. Depending on your usage pattern, you need to call this
|
158
|
+
# method periodically.
|
159
|
+
def gc
|
160
|
+
sync
|
161
|
+
mark
|
162
|
+
sweep
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return the object with the provided ID. This method is not part of the
|
166
|
+
# public API and should never be called by outside users. It's purely
|
167
|
+
# intended for internal use.
|
168
|
+
def object_by_id(id)
|
169
|
+
if (obj = @cache.object_by_id(id))
|
170
|
+
# We have the object in memory so we can just return it.
|
171
|
+
return obj
|
172
|
+
else
|
173
|
+
# We don't have the object in memory. Let's find it in the storage.
|
174
|
+
if @db.include?(id)
|
175
|
+
# Great, object found. Read it into memory and return it.
|
176
|
+
obj = ObjectBase::read(self, id)
|
177
|
+
# Add the object to the in-memory storage list.
|
178
|
+
@cache.cache_read(obj)
|
179
|
+
|
180
|
+
return obj
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# The requested object does not exist. Return nil.
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# This method can be used to check the database and optionally repair it.
|
189
|
+
# The repair is a pure structural repair. It cannot ensure that the stored
|
190
|
+
# data is still correct. E. g. if a reference to a non-existing or
|
191
|
+
# unreadable object is found, the reference will simply be deleted.
|
192
|
+
# @param repair [TrueClass/FalseClass] true if a repair attempt should be
|
193
|
+
# made.
|
194
|
+
def check(repair = true)
|
195
|
+
@db.clear_marks
|
196
|
+
# A buffer to hold a working set of object IDs.
|
197
|
+
stack = []
|
198
|
+
# First we check the top-level objects. They are only added to the
|
199
|
+
# working set if they are OK.
|
200
|
+
@root_objects.each do |name, id|
|
201
|
+
unless @db.check(id, repair)
|
202
|
+
stack << id
|
203
|
+
end
|
204
|
+
end
|
205
|
+
if repair
|
206
|
+
# Delete any top-level object that is defective.
|
207
|
+
stack.each { |id| @root_objects.delete(id) }
|
208
|
+
# The remaining top-level objects are the initial working set.
|
209
|
+
stack = @root_objects.values
|
210
|
+
else
|
211
|
+
# The initial working set must only be OK objects.
|
212
|
+
stack = @root_objects.values - stack
|
213
|
+
end
|
214
|
+
stack.each { |id| @db.mark(id) }
|
215
|
+
|
216
|
+
while !stack.empty?
|
217
|
+
id = stack.pop
|
218
|
+
(obj = object_by_id(id))._referenced_object_ids.each do |id|
|
219
|
+
# Add all found references that have passed the check to the working
|
220
|
+
# list for the next iterations.
|
221
|
+
if @db.check(id, repair)
|
222
|
+
unless @db.is_marked?(id)
|
223
|
+
stack << id
|
224
|
+
@db.mark(id)
|
225
|
+
end
|
226
|
+
elsif repair
|
227
|
+
# Remove references to bad objects.
|
228
|
+
obj._delete_reference_to_id(id)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# This method will execute the provided block as an atomic transaction
|
235
|
+
# regarding the manipulation of all objects associated with this Store. In
|
236
|
+
# case the execution of the block generates an exception, the transaction
|
237
|
+
# is aborted and all PEROBS objects are restored to the state at the
|
238
|
+
# beginning of the transaction. The exception is passed on to the
|
239
|
+
# enclosing scope, so you probably want to handle it accordingly.
|
240
|
+
def transaction
|
241
|
+
@cache.begin_transaction
|
242
|
+
begin
|
243
|
+
yield if block_given?
|
244
|
+
rescue => e
|
245
|
+
@cache.abort_transaction
|
246
|
+
raise e
|
247
|
+
end
|
248
|
+
@cache.end_transaction
|
249
|
+
end
|
250
|
+
|
251
|
+
# Calls the given block once for each object, passing that object as a
|
252
|
+
# parameter.
|
253
|
+
def each
|
254
|
+
@db.clear_marks
|
255
|
+
# Start with the object 0 and the indexes of the root objects. Push them
|
256
|
+
# onto the work stack.
|
257
|
+
stack = [ 0 ] + @root_objects.values
|
258
|
+
stack.each { |id| @db.mark(id) }
|
259
|
+
while !stack.empty?
|
260
|
+
# Get an object index from the stack.
|
261
|
+
obj = object_by_id(id = stack.pop)
|
262
|
+
yield(obj) if block_given?
|
263
|
+
obj._referenced_object_ids.each do |id|
|
264
|
+
unless @db.is_marked?(id)
|
265
|
+
@db.mark(id)
|
266
|
+
stack << id
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
|
274
|
+
# Mark phase of a mark-and-sweep garbage collector. It will mark all
|
275
|
+
# objects that are reachable from the root objects.
|
276
|
+
def mark
|
277
|
+
each
|
278
|
+
end
|
279
|
+
|
280
|
+
# Sweep phase of a mark-and-sweep garbage collector. It will remove all
|
281
|
+
# unmarked objects from the store.
|
282
|
+
def sweep
|
283
|
+
@db.delete_unmarked_objects
|
284
|
+
@cache.reset
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
|
data/lib/perobs.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# = perobs.rb -- Persistent Ruby Object Store
|
4
|
+
#
|
5
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
6
|
+
#
|
7
|
+
# MIT License
|
8
|
+
#
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
10
|
+
# a copy of this software and associated documentation files (the
|
11
|
+
# "Software"), to deal in the Software without restriction, including
|
12
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
13
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
14
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
15
|
+
# the following conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
22
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
24
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
25
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
26
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
27
|
+
|
28
|
+
require "perobs/version"
|
29
|
+
require 'perobs/Store'
|
data/perobs.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'perobs/version'
|
5
|
+
|
6
|
+
GEM_SPEC = Gem::Specification.new do |spec|
|
7
|
+
spec.name = "perobs"
|
8
|
+
spec.version = PEROBS::VERSION
|
9
|
+
spec.authors = ["Chris Schlaeger"]
|
10
|
+
spec.email = ["chris@linux.com"]
|
11
|
+
spec.summary = %q{Persistent Ruby Object Store}
|
12
|
+
spec.description = %q{Library to provide a persistent object store}
|
13
|
+
spec.homepage = "https://github.com/scrapper/perobs"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
21
|
+
spec.add_development_dependency 'yard', '~>0.8.7'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
end
|
data/spec/Array_spec.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
4
|
+
#
|
5
|
+
# MIT License
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
27
|
+
|
28
|
+
require 'fileutils'
|
29
|
+
require 'time'
|
30
|
+
|
31
|
+
require 'perobs'
|
32
|
+
|
33
|
+
class PO < PEROBS::Object
|
34
|
+
|
35
|
+
po_attr :name
|
36
|
+
|
37
|
+
def initialize(store, name = nil)
|
38
|
+
super(store)
|
39
|
+
set(:name, name)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe PEROBS::Array do
|
45
|
+
|
46
|
+
before(:all) do
|
47
|
+
@db_name = 'test_db'
|
48
|
+
FileUtils.rm_rf(@db_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
after(:each) do
|
52
|
+
FileUtils.rm_rf(@db_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should store simple objects persistently' do
|
56
|
+
store = PEROBS::Store.new(@db_name)
|
57
|
+
store['a'] = a = PEROBS::Array.new(store)
|
58
|
+
a[0] = 'A'
|
59
|
+
a[1] = 'B'
|
60
|
+
a[2] = po = PO.new(store)
|
61
|
+
po.name = 'foobar'
|
62
|
+
|
63
|
+
a[0].should == 'A'
|
64
|
+
a[1].should == 'B'
|
65
|
+
a[2].name.should == 'foobar'
|
66
|
+
store.sync
|
67
|
+
|
68
|
+
store = PEROBS::Store.new(@db_name)
|
69
|
+
a = store['a']
|
70
|
+
a[0].should == 'A'
|
71
|
+
a[1].should == 'B'
|
72
|
+
a[2].name.should == 'foobar'
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should have an each method to iterate' do
|
76
|
+
store = PEROBS::Store.new(@db_name)
|
77
|
+
store['a'] = a = PEROBS::Array.new(store)
|
78
|
+
a[0] = 'A'
|
79
|
+
a[1] = 'B'
|
80
|
+
a[2] = 'C'
|
81
|
+
vs = ''
|
82
|
+
a.each { |v| vs << v }
|
83
|
+
vs.should == 'ABC'
|
84
|
+
store.sync
|
85
|
+
|
86
|
+
store = PEROBS::Store.new(@db_name)
|
87
|
+
a = store['a']
|
88
|
+
vs = ''
|
89
|
+
a[3] = PO.new(store, 'D')
|
90
|
+
a.each { |v| vs << (v.is_a?(String) ? v : v.name) }
|
91
|
+
vs.should == 'ABCD'
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
4
|
+
#
|
5
|
+
# MIT License
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
27
|
+
|
28
|
+
require 'fileutils'
|
29
|
+
require 'time'
|
30
|
+
|
31
|
+
require 'perobs/FileSystemDB'
|
32
|
+
|
33
|
+
class TempStruct < Struct.new(:first, :second, :third)
|
34
|
+
end
|
35
|
+
|
36
|
+
describe PEROBS::FileSystemDB do
|
37
|
+
|
38
|
+
before(:all) do
|
39
|
+
FileUtils.rm_rf('fs_test')
|
40
|
+
end
|
41
|
+
|
42
|
+
after(:each) do
|
43
|
+
FileUtils.rm_rf('fs_test')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should create database' do
|
47
|
+
@db = PEROBS::FileSystemDB.new('fs_test')
|
48
|
+
Dir.exists?('fs_test').should be_true
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should support object insertion and retrieval' do
|
52
|
+
@db = PEROBS::FileSystemDB.new('fs_test')
|
53
|
+
@db.include?(0).should be_false
|
54
|
+
h = {
|
55
|
+
'String' => 'What god has wrought',
|
56
|
+
'Fixnum' => 42,
|
57
|
+
'Float' => 3.14,
|
58
|
+
'True' => true,
|
59
|
+
'False' => false,
|
60
|
+
'nil' => nil,
|
61
|
+
'Array' => [ 0, 1, 2, 3 ]
|
62
|
+
}
|
63
|
+
@db.put_object(h, 0)
|
64
|
+
@db.include?(0).should be_true
|
65
|
+
@db.check(0, false).should be_true
|
66
|
+
@db.get_object(0).should == h
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should support most Ruby objects types' do
|
70
|
+
[ :marshal, :yaml ].each do |serializer|
|
71
|
+
@db = PEROBS::FileSystemDB.new('fs_test', { :serializer => serializer })
|
72
|
+
@db.include?(0).should be_false
|
73
|
+
h = {
|
74
|
+
'String' => 'What god has wrought',
|
75
|
+
'Fixnum' => 42,
|
76
|
+
'Float' => 3.14,
|
77
|
+
'True' => true,
|
78
|
+
'False' => false,
|
79
|
+
'nil' => nil,
|
80
|
+
'Array' => [ 0, 1, 2, 3 ],
|
81
|
+
'Time' => Time.parse('2015-05-14-13:52:17'),
|
82
|
+
'Struct' => TempStruct.new("Where's", 'your', 'towel?')
|
83
|
+
}
|
84
|
+
@db.put_object(h, 0)
|
85
|
+
@db.include?(0).should be_true
|
86
|
+
@db.check(0, false).should be_true
|
87
|
+
@db.get_object(0).should == h
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should mark objects and detect markings' do
|
92
|
+
@db = PEROBS::FileSystemDB.new('fs_test')
|
93
|
+
h = { 'a' => 'z' }
|
94
|
+
@db.put_object(h, 1)
|
95
|
+
@db.put_object(h, 2)
|
96
|
+
@db.clear_marks
|
97
|
+
@db.is_marked?(1).should be_false
|
98
|
+
@db.mark(1)
|
99
|
+
@db.is_marked?(1).should be_true
|
100
|
+
|
101
|
+
@db.include?(2).should be_true
|
102
|
+
@db.delete_unmarked_objects
|
103
|
+
@db.include?(1).should be_true
|
104
|
+
@db.include?(2).should be_false
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
data/spec/Hash_spec.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
#
|
3
|
+
# Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
|
4
|
+
#
|
5
|
+
# MIT License
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
# a copy of this software and associated documentation files (the
|
9
|
+
# "Software"), to deal in the Software without restriction, including
|
10
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
# the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be
|
16
|
+
# included in all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
21
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
22
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
23
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
24
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
27
|
+
|
28
|
+
require 'fileutils'
|
29
|
+
require 'time'
|
30
|
+
|
31
|
+
require 'perobs'
|
32
|
+
|
33
|
+
|
34
|
+
class PO < PEROBS::Object
|
35
|
+
|
36
|
+
po_attr :name
|
37
|
+
|
38
|
+
def initialize(store, name = nil)
|
39
|
+
super(store)
|
40
|
+
@name = name
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
describe PEROBS::Hash do
|
46
|
+
|
47
|
+
before(:all) do
|
48
|
+
@db_name = 'test_db'
|
49
|
+
FileUtils.rm_rf(@db_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
after(:each) do
|
53
|
+
FileUtils.rm_rf(@db_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should store simple objects persistently' do
|
57
|
+
store = PEROBS::Store.new(@db_name)
|
58
|
+
store['h'] = h = PEROBS::Hash.new(store)
|
59
|
+
h['a'] = 'A'
|
60
|
+
h['b'] = 'B'
|
61
|
+
h['po'] = po = PO.new(store)
|
62
|
+
po.name = 'foobar'
|
63
|
+
h['b'] = 'B'
|
64
|
+
|
65
|
+
h['a'].should == 'A'
|
66
|
+
h['b'].should == 'B'
|
67
|
+
store.sync
|
68
|
+
|
69
|
+
store = PEROBS::Store.new(@db_name)
|
70
|
+
h = store['h']
|
71
|
+
h['a'].should == 'A'
|
72
|
+
h['b'].should == 'B'
|
73
|
+
h['po'].name.should == 'foobar'
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should have an each method to iterate' do
|
77
|
+
store = PEROBS::Store.new(@db_name)
|
78
|
+
store['h'] = h = PEROBS::Hash.new(store)
|
79
|
+
h['a'] = 'A'
|
80
|
+
h['b'] = 'B'
|
81
|
+
h['c'] = 'C'
|
82
|
+
vs = []
|
83
|
+
h.each { |k, v| vs << k + v }
|
84
|
+
vs.sort.join.should == 'aAbBcC'
|
85
|
+
|
86
|
+
store = PEROBS::Store.new(@db_name)
|
87
|
+
store['h'] = h = PEROBS::Hash.new(store)
|
88
|
+
h['a'] = PO.new(store, 'A')
|
89
|
+
h['b'] = PO.new(store, 'B')
|
90
|
+
h['c'] = PO.new(store, 'C')
|
91
|
+
vs = []
|
92
|
+
h.each { |k, v| vs << k + v.name }
|
93
|
+
vs.sort.join.should == 'aAbBcC'
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|