perobs 0.0.1

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