perobs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+
@@ -0,0 +1,4 @@
1
+ module PEROBS
2
+ # The version number
3
+ VERSION = "0.0.1"
4
+ end
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
@@ -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