perobs 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/perobs.rb +1 -0
  3. data/lib/perobs/Array.rb +66 -19
  4. data/lib/perobs/BTree.rb +83 -12
  5. data/lib/perobs/BTreeBlob.rb +1 -1
  6. data/lib/perobs/BTreeDB.rb +2 -2
  7. data/lib/perobs/BTreeNode.rb +365 -85
  8. data/lib/perobs/BigArray.rb +267 -0
  9. data/lib/perobs/BigArrayNode.rb +998 -0
  10. data/lib/perobs/BigHash.rb +262 -0
  11. data/lib/perobs/BigTree.rb +184 -0
  12. data/lib/perobs/BigTreeNode.rb +873 -0
  13. data/lib/perobs/ConsoleProgressMeter.rb +61 -0
  14. data/lib/perobs/DataBase.rb +4 -3
  15. data/lib/perobs/DynamoDB.rb +57 -15
  16. data/lib/perobs/EquiBlobsFile.rb +143 -51
  17. data/lib/perobs/FNV_Hash_1a_64.rb +54 -0
  18. data/lib/perobs/FlatFile.rb +363 -203
  19. data/lib/perobs/FlatFileBlobHeader.rb +98 -54
  20. data/lib/perobs/FlatFileDB.rb +42 -20
  21. data/lib/perobs/Hash.rb +58 -13
  22. data/lib/perobs/IDList.rb +144 -0
  23. data/lib/perobs/IDListPage.rb +107 -0
  24. data/lib/perobs/IDListPageFile.rb +180 -0
  25. data/lib/perobs/IDListPageRecord.rb +142 -0
  26. data/lib/perobs/Object.rb +18 -15
  27. data/lib/perobs/ObjectBase.rb +38 -4
  28. data/lib/perobs/PersistentObjectCache.rb +53 -67
  29. data/lib/perobs/PersistentObjectCacheLine.rb +24 -12
  30. data/lib/perobs/ProgressMeter.rb +97 -0
  31. data/lib/perobs/SpaceTree.rb +21 -12
  32. data/lib/perobs/SpaceTreeNode.rb +53 -61
  33. data/lib/perobs/Store.rb +71 -32
  34. data/lib/perobs/version.rb +1 -1
  35. data/perobs.gemspec +4 -4
  36. data/test/Array_spec.rb +15 -6
  37. data/test/BTree_spec.rb +5 -2
  38. data/test/BigArray_spec.rb +214 -0
  39. data/test/BigHash_spec.rb +144 -0
  40. data/test/BigTreeNode_spec.rb +153 -0
  41. data/test/BigTree_spec.rb +259 -0
  42. data/test/EquiBlobsFile_spec.rb +105 -1
  43. data/test/FNV_Hash_1a_64_spec.rb +59 -0
  44. data/test/FlatFileDB_spec.rb +63 -14
  45. data/test/Hash_spec.rb +1 -2
  46. data/test/IDList_spec.rb +77 -0
  47. data/test/LegacyDBs/LegacyDB.rb +151 -0
  48. data/test/LegacyDBs/version_3/class_map.json +1 -0
  49. data/test/LegacyDBs/version_3/config.json +1 -0
  50. data/test/LegacyDBs/version_3/database.blobs +0 -0
  51. data/test/LegacyDBs/version_3/database_spaces.blobs +0 -0
  52. data/test/LegacyDBs/version_3/index.blobs +0 -0
  53. data/test/LegacyDBs/version_3/version +1 -0
  54. data/test/LockFile_spec.rb +9 -6
  55. data/test/SpaceTree_spec.rb +4 -1
  56. data/test/Store_spec.rb +290 -199
  57. data/test/spec_helper.rb +9 -4
  58. metadata +47 -10
  59. data/lib/perobs/TreeDB.rb +0 -277
@@ -0,0 +1,59 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2019 by Chris Schlaeger <chris@taskjuggler.org>
4
+ #
5
+ # This file contains tests for Array that are similar to the tests for the
6
+ # Array implementation in MRI. The ideas of these tests were replicated in
7
+ # this code.
8
+ #
9
+ # MIT License
10
+ #
11
+ # Permission is hereby granted, free of charge, to any person obtaining
12
+ # a copy of this software and associated documentation files (the
13
+ # "Software"), to deal in the Software without restriction, including
14
+ # without limitation the rights to use, copy, modify, merge, publish,
15
+ # distribute, sublicense, and/or sell copies of the Software, and to
16
+ # permit persons to whom the Software is furnished to do so, subject to
17
+ # the following conditions:
18
+ #
19
+ # The above copyright notice and this permission notice shall be
20
+ # included in all copies or substantial portions of the Software.
21
+ #
22
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29
+
30
+ require 'spec_helper'
31
+
32
+ require 'perobs'
33
+
34
+ describe PEROBS::FNV_Hash_1a_64 do
35
+
36
+ it 'should generate stable hashes for Strings' do
37
+ refs = [
38
+ [ 'foo', 15902901984413996407 ],
39
+ [ 'foo', 15902901984413996407 ],
40
+ [ 'bar', 16101355973854746 ],
41
+ [ 'foobar', 9625390261332436968 ],
42
+ [ 'PEROBS rocks your application!', 4089220442501866848 ],
43
+ [ 'Permission is hereby granted, free of charge, to any person ' +
44
+ 'obtaining a copy of this software and associated documentation ' +
45
+ 'files (the "Software"), to deal in the Software without ' +
46
+ 'restriction, including without limitation the rights to use, ' +
47
+ 'copy, modify, merge, publish, distribute, sublicense, and/or ' +
48
+ 'sell copies of the Software, and to permit persons to whom the ' +
49
+ 'Software is furnished to do so, subject to the following conditions:',
50
+ 17637146001033534275 ]
51
+ ]
52
+
53
+ refs.each do |v|
54
+ expect(PEROBS::FNV_Hash_1a_64::digest(v[0])).to eql(v[1])
55
+ end
56
+ end
57
+
58
+ end
59
+
@@ -28,6 +28,7 @@ require 'fileutils'
28
28
  require 'spec_helper'
29
29
  require 'perobs/FlatFileDB'
30
30
  require 'perobs/Store'
31
+ require 'LegacyDBs/LegacyDB'
31
32
 
32
33
  class FlatFileDB_O < PEROBS::Object
33
34
 
@@ -47,7 +48,12 @@ describe PEROBS::FlatFileDB do
47
48
  before(:each) do
48
49
  @db_dir = generate_db_name(__FILE__)
49
50
  FileUtils.mkdir_p(@db_dir)
50
- @store = PEROBS::Store.new(@db_dir, :engine => PEROBS::FlatFileDB)
51
+ @store_options = {
52
+ :engine => PEROBS::FlatFileDB,
53
+ :log => $stderr,
54
+ :log_level => Logger::ERROR
55
+ }
56
+ @store = PEROBS::Store.new(@db_dir, @store_options)
51
57
  end
52
58
 
53
59
  after(:each) do
@@ -67,17 +73,13 @@ describe PEROBS::FlatFileDB do
67
73
 
68
74
  it 'should do a version upgrade' do
69
75
  # Close the store
70
- @store['o'] = @store.new(FlatFileDB_O)
71
76
  @store.exit
77
+ src_dir = File.join(File.dirname(__FILE__), 'LegacyDBs', 'version_3')
78
+ FileUtils.cp_r(Dir.glob(src_dir + '/*'), @db_dir)
72
79
 
73
- # Manually downgrade the version file to version 1
74
- version_file = File.join(@db_dir, 'version')
75
- File.write(version_file, '1')
76
-
77
- # Open the store again
78
- store = PEROBS::Store.new(@db_dir, :engine => PEROBS::FlatFileDB)
79
- expect(File.read(version_file).to_i).to eql(PEROBS::FlatFileDB::VERSION)
80
- expect(store['o'].b).to eql(42)
80
+ db = LegacyDB.new(@db_dir)
81
+ capture_io { db.open }
82
+ capture_io { expect(db.check).to be true }
81
83
  end
82
84
 
83
85
  it 'should refuse a version downgrade' do
@@ -89,7 +91,7 @@ describe PEROBS::FlatFileDB do
89
91
  File.write(version_file, '1000000')
90
92
 
91
93
  # Open the store again
92
- expect { PEROBS::Store.new(@db_dir, :engine => PEROBS::FlatFileDB) }.to raise_error(PEROBS::FatalError)
94
+ expect { PEROBS::Store.new(@db_dir) }.to raise_error(PEROBS::FatalError)
93
95
  end
94
96
 
95
97
  it 'should recover from a lost index file' do
@@ -97,7 +99,10 @@ describe PEROBS::FlatFileDB do
97
99
  @store.exit
98
100
 
99
101
  File.delete(File.join(@db_dir, 'index.blobs'))
100
- store = PEROBS::Store.new(@db_dir, :engine => PEROBS::FlatFileDB)
102
+ store = nil
103
+ capture_io do
104
+ store = PEROBS::Store.new(@db_dir)
105
+ end
101
106
  expect(store['o'].b).to eql(42)
102
107
  end
103
108
 
@@ -106,10 +111,54 @@ describe PEROBS::FlatFileDB do
106
111
  @store.exit
107
112
 
108
113
  File.write(File.join(@db_dir, 'index.blobs'), '*' * 500)
109
- store = PEROBS::Store.new(@db_dir, :engine => PEROBS::FlatFileDB)
110
- store.check(true)
114
+ store = nil
115
+ capture_io do
116
+ store = PEROBS::Store.new(@db_dir)
117
+ end
118
+ capture_io { store.check(true) }
111
119
  expect(store['o'].b).to eql(42)
112
120
  end
113
121
 
122
+ it 'should repair a corrupted database.blobs file' do
123
+ @store.exit
124
+
125
+ db = PEROBS::FlatFileDB.new(@db_dir)
126
+ db_file = File.join(@db_dir, 'database.blobs')
127
+ db.open
128
+ 0.upto(5) do |i|
129
+ db.put_object("#{i + 1}:#{'X' * (i + 1) * 30}", i + 1)
130
+ end
131
+ db.close
132
+ db.open
133
+ 0.upto(5) do |i|
134
+ db.put_object("#{i + 10}:#{'Y' * (i + 1) * 25}", i + 10)
135
+ end
136
+ pos = db.instance_variable_get('@flat_file').find_obj_addr_by_id(10)
137
+ db.close
138
+
139
+ f = File.open(db_file, 'rb+')
140
+ f.seek(pos)
141
+ f.write('ZZZZZ')
142
+ f.close
143
+
144
+ db.open
145
+ expect(db.check_db).to eql(2)
146
+ expect(db.check_db(true)).to eql(1)
147
+ db.close
148
+ db = PEROBS::FlatFileDB.new(@db_dir, { :log => $stderr,
149
+ :log_level => Logger::ERROR })
150
+ db.open
151
+ expect(db.check_db).to eql(0)
152
+
153
+ 0.upto(5) do |i|
154
+ expect(db.get_object(i + 1)).to eql("#{i + 1}:#{'X' * (i + 1) * 30}")
155
+ end
156
+ expect(db.get_object(10)).to be_nil
157
+ 1.upto(5) do |i|
158
+ expect(db.get_object(i + 10)).to eql("#{i + 10}:#{'Y' * (i + 1) * 25}")
159
+ end
160
+ db.close
161
+ end
162
+
114
163
  end
115
164
 
@@ -169,9 +169,8 @@ describe PEROBS::Hash do
169
169
  it 'should catch a leaked PEROBS::ObjectBase object' do
170
170
  @store['a'] = a = @store.new(PEROBS::Hash)
171
171
  o = @store.new(PO)
172
- a['a'] = o.get_self
173
172
  PEROBS.log.open(StringIO.new)
174
- expect { @store.sync }.to raise_error(PEROBS::FatalError)
173
+ expect { a['a'] = o.get_self }.to raise_error(PEROBS::FatalError)
175
174
  PEROBS.log.open($stderr)
176
175
  end
177
176
 
@@ -0,0 +1,77 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2016, 2017 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
+ require 'spec_helper'
27
+ require 'perobs/IDList'
28
+
29
+ module PEROBS
30
+
31
+ describe IDList do
32
+
33
+ before(:all) do
34
+ @db_dir = generate_db_name('IDList')
35
+ FileUtils.mkdir_p(@db_dir)
36
+ @list = PEROBS::IDList.new(@db_dir, 'idlist', 16, 64)
37
+ end
38
+
39
+ after(:all) do
40
+ @list.erase
41
+ FileUtils.rm_rf(@db_dir)
42
+ end
43
+
44
+ it 'should not contain any values' do
45
+ expect(@list.to_a).to eql []
46
+ expect(@list.include?(0)).to be false
47
+ expect(@list.include?(1)).to be false
48
+ expect { @list.check }.to_not raise_error
49
+ end
50
+
51
+ it 'should store a large number of values' do
52
+ vals = []
53
+ 50000.times do
54
+ v = rand(2 ** 64)
55
+ vals << v
56
+
57
+ next if @list.include?(v)
58
+ @list.insert(v)
59
+ #expect(@list.include?(v)).to be true
60
+ 0.upto(rand(10)) do
61
+ v = vals[rand(vals.length)]
62
+ expect(@list.include?(v)).to be true
63
+ end
64
+
65
+ #expect { @list.check }.to_not raise_error if rand(1000) == 0
66
+ end
67
+ expect { @list.check }.to_not raise_error
68
+
69
+ vals.each do |v|
70
+ expect(@list.include?(v)).to be true
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
@@ -0,0 +1,151 @@
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 'perobs'
29
+
30
+ # This class creates and manages a simple DB with some toy data to check the
31
+ # conversion routines for legacy DB formats.
32
+ class LegacyDB
33
+
34
+ class Fragment < PEROBS::Object
35
+
36
+ attr_persist :str, :pred, :succ
37
+
38
+ def initialize(p, str, pred = nil)
39
+ super(p)
40
+ self.str = str
41
+ self.pred = pred
42
+ self.succ = nil
43
+ end
44
+
45
+ end
46
+
47
+ N1 = 293
48
+ N2 = 427
49
+
50
+ def initialize(name)
51
+ @name = name
52
+ @store = nil
53
+ end
54
+
55
+ def create
56
+ @store = PEROBS::Store.new(@name)
57
+ @store['fragments'] = @store.new(PEROBS::Array)
58
+ @store['metadata'] = @store.new(PEROBS::Hash)
59
+ @store['by_length'] = @store.new(PEROBS::Hash)
60
+
61
+ # Create a long string of digits.
62
+ number = (N1**N2).to_s
63
+ # Find a suitable digit that we can use a separator to split the long
64
+ # string into smaller strings.
65
+ separator = find_separator(number)
66
+ @store['metadata']['separator'] = separator
67
+ pred = nil
68
+ # Store all the fragments in the @store['fragments'] array.
69
+ number.split(separator).each do |fragment|
70
+ @store['fragments'] << (f = @store.new(Fragment, fragment, pred))
71
+ # Additionally, we create the doubly-linked list of the fragments.
72
+ pred.succ = f if pred
73
+ pred = f
74
+ # And we store the fragments hashed by their length.
75
+ length = fragment.length.to_s
76
+ if @store['by_length'][length].nil?
77
+ @store['by_length'][length] = @store.new(PEROBS::Array)
78
+ end
79
+ @store['by_length'][length] << f
80
+ end
81
+ @store.exit
82
+ end
83
+
84
+ def open
85
+ @store = PEROBS::Store.new(@name)
86
+ end
87
+
88
+ def check
89
+ # Recreate the original number from the @store['fragments'] list.
90
+ number = @store['fragments'].map { |f| f.str }.
91
+ join(@store['metadata']['separator'])
92
+ if number.to_i != N1 ** N2
93
+ raise RuntimeError, "Number mismatch\n#{number}\n#{N1 ** N2}"
94
+ end
95
+
96
+ # Check the total number of digits based on the bash by length.
97
+ fragment_counter = 0
98
+ total_fragment_length = 0
99
+ @store['by_length'].each do |length, fragments|
100
+ fragment_counter += fragments.length
101
+ total_fragment_length += length.to_i * fragments.length
102
+ end
103
+ if number.length != total_fragment_length + fragment_counter - 1
104
+ raise RuntimeError, "Number length mismatch"
105
+ end
106
+
107
+ # Recreate the original number from the linked list forward traversal.
108
+ number = ''
109
+ f = @store['fragments'][0]
110
+ while f
111
+ number += @store['metadata']['separator'] unless number.empty?
112
+ number += f.str
113
+ f = f.succ
114
+ end
115
+ if number.to_i != N1 ** N2
116
+ raise RuntimeError, "Number mismatch\n#{number}\n#{N1 ** N2}"
117
+ end
118
+
119
+ # Recreate the original number from the linked list backwards traversal.
120
+ number = ''
121
+ f = @store['fragments'][-1]
122
+ while f
123
+ number = @store['metadata']['separator'] + number unless number.empty?
124
+ number = f.str + number
125
+ f = f.pred
126
+ end
127
+ if number.to_i != N1 ** N2
128
+ raise RuntimeError, "Number mismatch\n#{number}\n#{N1 ** N2}"
129
+ end
130
+
131
+ true
132
+ end
133
+
134
+ private
135
+
136
+ def find_separator(str)
137
+ 0.upto(9) do |digit|
138
+ c = digit.to_s
139
+ return c if str[0] != c && str[-1] != c
140
+ end
141
+
142
+ raise RuntimeError, "Could not find separator"
143
+ end
144
+
145
+ end
146
+
147
+ #db = LegacyDB.new('test')
148
+ #db.create
149
+ #db.open
150
+ #db.check
151
+
@@ -0,0 +1 @@
1
+ {"PEROBS::Hash":0,"PEROBS::Array":1,"LegacyDB::Fragment":2}
@@ -0,0 +1 @@
1
+ {"serializer":{"json_class":"Symbol","s":"json"}}
@@ -30,7 +30,11 @@ require 'perobs/LockFile'
30
30
  describe PEROBS::LockFile do
31
31
 
32
32
  before(:each) do
33
- @dir = Dir.mktmpdir('LockFile')
33
+ PEROBS.log.open($stderr)
34
+ PEROBS.log.level = Logger::INFO
35
+ @dir = File.join(Dir.tmpdir,
36
+ "#{File.basename('LockFile_spec')}.#{rand(2**32)}")
37
+ FileUtils.mkdir_p(@dir)
34
38
  @file = File.join(@dir, 'LockFile.lock')
35
39
  end
36
40
 
@@ -42,7 +46,6 @@ describe PEROBS::LockFile do
42
46
  capture_io do
43
47
  expect(PEROBS::LockFile.new('/foo/bar/foobar').lock).to be false
44
48
  end
45
- PEROBS.log.open($stderr)
46
49
  end
47
50
 
48
51
  it 'should support taking and releasing the lock' do
@@ -59,7 +62,7 @@ describe PEROBS::LockFile do
59
62
  expect(lock.is_locked?).to be true
60
63
  lock.forced_unlock
61
64
  expect(lock.is_locked?).to be false
62
- out = capture_io{ expect(lock.unlock).to be false }
65
+ out = capture_io{ expect(lock.unlock).to be false }.log
63
66
  expect(out).to include('There is no current lock to release')
64
67
  end
65
68
 
@@ -67,7 +70,7 @@ describe PEROBS::LockFile do
67
70
  lock1 = PEROBS::LockFile.new(@file)
68
71
  expect(lock1.lock).to be true
69
72
  lock2 = PEROBS::LockFile.new(@file)
70
- out = capture_io { expect(lock2.lock).to be false }
73
+ out = capture_io { expect(lock2.lock).to be false }.log
71
74
  expect(out).to include('due to timeout')
72
75
  expect(lock1.unlock).to be true
73
76
  expect(lock2.lock).to be true
@@ -105,7 +108,7 @@ describe PEROBS::LockFile do
105
108
  end
106
109
  lock2 = PEROBS::LockFile.new(@file,
107
110
  { :max_retries => 2, :pause_secs => 0.5 })
108
- out = capture_io { expect(lock2.lock).to be false }
111
+ out = capture_io { expect(lock2.lock).to be false }.log
109
112
  expect(out).to include('due to timeout')
110
113
  Process.wait(pid)
111
114
  end
@@ -123,7 +126,7 @@ describe PEROBS::LockFile do
123
126
  end
124
127
 
125
128
  lock2 = PEROBS::LockFile.new(@file, { :timeout_secs => 1 })
126
- out = capture_io { expect(lock2.lock).to be true }
129
+ out = capture_io { expect(lock2.lock).to be true }.log
127
130
  expect(out).to include('Old lock file found for PID')
128
131
  expect(lock2.unlock).to be true
129
132
  Process.wait(pid)