fsdb 0.6.1 → 0.7.0
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/History.txt +10 -0
- data/{README.txt → README.markdown} +137 -126
- data/bench/bench.rb +1 -1
- data/examples/forks.rb +43 -0
- data/lib/fsdb/database.rb +5 -16
- data/lib/fsdb/file-lock.rb +15 -58
- data/lib/fsdb/modex.rb +39 -42
- data/test/test-concurrency.rb +2 -2
- data/test/test-concurrency/init.rb +0 -4
- data/test/test-formats.rb +1 -1
- data/test/test-fsdb.rb +3 -1
- data/test/test-modex.rb +64 -20
- data/test/test.rb +1 -7
- metadata +56 -98
- data/ext/fsdb/MANIFEST +0 -1
- data/ext/fsdb/extconf.rb +0 -9
- data/ext/fsdb/fcntl-lock.c +0 -112
- data/lib/fsdb/compat.rb +0 -42
- data/lib/fsdb/faster-modex.rb +0 -223
- data/lib/fsdb/faster-mutex.rb +0 -138
- data/lib/fsdb/mutex.rb +0 -137
- data/rakefile +0 -41
- data/tasks/ann.rake +0 -80
- data/tasks/bones.rake +0 -20
- data/tasks/gem.rake +0 -201
- data/tasks/git.rake +0 -40
- data/tasks/notes.rake +0 -27
- data/tasks/post_load.rake +0 -34
- data/tasks/rdoc.rake +0 -51
- data/tasks/rubyforge.rake +0 -55
- data/tasks/setup.rb +0 -292
- data/tasks/spec.rake +0 -54
- data/tasks/svn.rake +0 -47
- data/tasks/test.rake +0 -40
- data/tasks/zentest.rake +0 -36
- data/test/err.txt +0 -31
- data/test/test-mutex.rb +0 -33
data/bench/bench.rb
CHANGED
data/examples/forks.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'fsdb'
|
2
|
+
|
3
|
+
db = FSDB::Database.new '/tmp/fsdb-example'
|
4
|
+
|
5
|
+
db['foo.txt'] = "hello, world"
|
6
|
+
puts db['foo.txt']
|
7
|
+
puts
|
8
|
+
|
9
|
+
p1 = fork do
|
10
|
+
puts "*** starting fork #1 at #{Time.now}"
|
11
|
+
db.edit 'foo.txt' do |str|
|
12
|
+
str << "\n edited by fork #1 at #{Time.now}"
|
13
|
+
sleep 2 # give fork #2 a chance to try editing
|
14
|
+
str << "\n done editing in fork #1 at #{Time.now}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
sleep 1
|
19
|
+
|
20
|
+
p2 = fork do
|
21
|
+
puts "*** starting fork #2 at #{Time.now}"
|
22
|
+
db.edit 'foo.txt' do |str|
|
23
|
+
str << "\n edited by fork #2 at #{Time.now}"
|
24
|
+
str << "\n done editing in fork #2 at #{Time.now}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Process.waitpid p1
|
29
|
+
Process.waitpid p2
|
30
|
+
|
31
|
+
puts db['foo.txt']
|
32
|
+
|
33
|
+
__END__
|
34
|
+
|
35
|
+
hello, world
|
36
|
+
|
37
|
+
*** starting fork #1 at Tue Nov 29 11:03:03 -0800 2011
|
38
|
+
*** starting fork #2 at Tue Nov 29 11:03:04 -0800 2011
|
39
|
+
hello, world
|
40
|
+
edited by fork #1 at Tue Nov 29 11:03:03 -0800 2011
|
41
|
+
done editing in fork #1 at Tue Nov 29 11:03:05 -0800 2011
|
42
|
+
edited by fork #2 at Tue Nov 29 11:03:05 -0800 2011
|
43
|
+
done editing in fork #2 at Tue Nov 29 11:03:05 -0800 2011
|
data/lib/fsdb/database.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
require 'fsdb/platform'
|
3
|
-
require 'fsdb/compat' if RUBY_VERSION.to_f < 1.7
|
4
|
-
require 'fsdb/mutex'
|
5
3
|
require 'fsdb/modex'
|
6
4
|
require 'fsdb/file-lock'
|
7
5
|
require 'fsdb/formats'
|
@@ -9,7 +7,7 @@ require 'fsdb/formats'
|
|
9
7
|
module FSDB
|
10
8
|
include Formats
|
11
9
|
|
12
|
-
FSDB::VERSION = "0.
|
10
|
+
FSDB::VERSION = "0.7.0"
|
13
11
|
|
14
12
|
# A thread-safe, process-safe object database class which uses the
|
15
13
|
# native file system as its back end and allows multiple file formats.
|
@@ -108,14 +106,10 @@ class Database
|
|
108
106
|
|
109
107
|
# Subclasses can change the defaults.
|
110
108
|
DEFAULT_META_PREFIX = '..fsdb.meta.'
|
111
|
-
|
112
|
-
### DEFAULT_LOCK_TYPE = :fcntl_lock
|
113
|
-
### else
|
114
|
-
DEFAULT_LOCK_TYPE = :flock
|
115
|
-
### end
|
109
|
+
DEFAULT_LOCK_TYPE = :flock
|
116
110
|
|
117
111
|
# These must be methods of File.
|
118
|
-
LOCK_TYPES = [:flock
|
112
|
+
LOCK_TYPES = [:flock] # obsolete: :fcntl_lock
|
119
113
|
|
120
114
|
@cache = {} # maps <file id> to <CacheEntry>
|
121
115
|
@cache_mutex = Mutex.new # protects access to @cache hash
|
@@ -130,15 +124,14 @@ class Database
|
|
130
124
|
# The root directory of the db, to which paths are relative.
|
131
125
|
attr_reader :dir
|
132
126
|
|
133
|
-
# The lock type of the db, by default <tt>:flock</tt
|
134
|
-
# <tt>:fcntl_lock</tt>.
|
127
|
+
# The lock type of the db, by default <tt>:flock</tt>.
|
135
128
|
attr_reader :lock_type
|
136
129
|
|
137
130
|
# Create a new database object that accesses +dir+. Makes sure that the
|
138
131
|
# directory exists on disk, but doesn't create or open any other files.
|
139
132
|
# The +opts+ hash can include:
|
140
133
|
#
|
141
|
-
# <tt>:lock_type</tt>:: <tt>:flock</tt> by default
|
134
|
+
# <tt>:lock_type</tt>:: <tt>:flock</tt> by default
|
142
135
|
#
|
143
136
|
# <tt>:meta_prefix</tt>:: <tt>'..fsdb.meta.'</tt> by default
|
144
137
|
#
|
@@ -152,10 +145,6 @@ class Database
|
|
152
145
|
raise "Unknown lock type: #{lock_type}"
|
153
146
|
end
|
154
147
|
|
155
|
-
if @lock_type == :fcntl_lock
|
156
|
-
require 'fcntl_lock' ## hack.
|
157
|
-
end
|
158
|
-
|
159
148
|
@meta_prefix = opts[:meta_prefix] || DEFAULT_META_PREFIX
|
160
149
|
|
161
150
|
@formats = opts[:formats]
|
data/lib/fsdb/file-lock.rb
CHANGED
@@ -7,10 +7,6 @@ class File
|
|
7
7
|
CAN_DELETE_OPEN_FILE = !FSDB::PLATFORM_IS_WINDOWS
|
8
8
|
CAN_OPEN_DIR = !FSDB::PLATFORM_IS_WINDOWS
|
9
9
|
|
10
|
-
LOCK_BLOCK_FIXED_VER = "1.8.2" # Hurray!
|
11
|
-
LOCK_DOESNT_BLOCK = [RUBY_VERSION, LOCK_BLOCK_FIXED_VER].
|
12
|
-
map {|s| s.split('.')}.sort[0].join('.') == LOCK_BLOCK_FIXED_VER
|
13
|
-
|
14
10
|
if FSDB::PLATFORM_IS_WINDOWS_ME
|
15
11
|
# no flock() on WinME
|
16
12
|
|
@@ -21,61 +17,22 @@ class File
|
|
21
17
|
end
|
22
18
|
|
23
19
|
else
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
# Get a shared (i.e., read) lock on the file.
|
35
|
-
# If the lock is not available, wait for it without blocking other ruby
|
36
|
-
# threads.
|
37
|
-
def lock_shared lock_type
|
38
|
-
send(lock_type, LOCK_SH)
|
39
|
-
rescue Errno::EINTR
|
40
|
-
retry
|
41
|
-
end
|
42
|
-
|
43
|
-
else
|
44
|
-
def lock_exclusive lock_type
|
45
|
-
if Thread.list.size == 1
|
46
|
-
begin
|
47
|
-
send(lock_type, LOCK_EX)
|
48
|
-
rescue Errno::EINTR
|
49
|
-
retry
|
50
|
-
end
|
51
|
-
else
|
52
|
-
# ugly hack because waiting for a lock in a Ruby thread blocks the
|
53
|
-
# entire process
|
54
|
-
period = 0.001
|
55
|
-
until send(lock_type, LOCK_EX|LOCK_NB)
|
56
|
-
sleep period
|
57
|
-
period *= 2 if period < 1
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
20
|
+
# Get an exclusive (i.e., write) lock on the file.
|
21
|
+
# If the lock is not available, wait for it without blocking other ruby
|
22
|
+
# threads.
|
23
|
+
def lock_exclusive lock_type
|
24
|
+
send(lock_type, LOCK_EX)
|
25
|
+
rescue Errno::EINTR
|
26
|
+
retry
|
27
|
+
end
|
61
28
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
else
|
70
|
-
# ugly hack because waiting for a lock in a Ruby thread blocks the
|
71
|
-
# entire process
|
72
|
-
period = 0.001
|
73
|
-
until send(lock_type, LOCK_SH|LOCK_NB)
|
74
|
-
sleep period
|
75
|
-
period *= 2 if period < 1
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
29
|
+
# Get a shared (i.e., read) lock on the file.
|
30
|
+
# If the lock is not available, wait for it without blocking other ruby
|
31
|
+
# threads.
|
32
|
+
def lock_shared lock_type
|
33
|
+
send(lock_type, LOCK_SH)
|
34
|
+
rescue Errno::EINTR
|
35
|
+
retry
|
79
36
|
end
|
80
37
|
end
|
81
38
|
|
data/lib/fsdb/modex.rb
CHANGED
@@ -1,30 +1,6 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
unless defined? Thread.exclusive
|
4
|
-
class Thread # :nodoc:
|
5
|
-
def self.exclusive
|
6
|
-
old = critical
|
7
|
-
self.critical = true
|
8
|
-
yield
|
9
|
-
ensure
|
10
|
-
self.critical = old
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
class Thread
|
16
|
-
def self.nonexclusive
|
17
|
-
old = critical
|
18
|
-
self.critical = false
|
19
|
-
yield
|
20
|
-
ensure
|
21
|
-
self.critical = old
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
1
|
module FSDB
|
26
2
|
|
27
|
-
# Modex is a modal exclusion semaphore
|
3
|
+
# Modex is a modal exclusion semaphore.
|
28
4
|
# The two modes are shared (SH) and exclusive (EX).
|
29
5
|
# Modex is not nestable.
|
30
6
|
#
|
@@ -37,12 +13,13 @@ class Modex
|
|
37
13
|
@locked = []
|
38
14
|
@mode = nil
|
39
15
|
@first = true
|
16
|
+
@m = Mutex.new
|
40
17
|
end
|
41
18
|
|
42
19
|
def try_lock mode
|
43
|
-
|
20
|
+
@m.synchronize do
|
44
21
|
thread = Thread.current
|
45
|
-
raise ThreadError if @locked.include?(thread)
|
22
|
+
raise ThreadError, "nesting not allowed" if @locked.include?(thread)
|
46
23
|
|
47
24
|
if @mode == mode and mode == SH and @waiting.empty? # strict queue
|
48
25
|
@locked << thread
|
@@ -51,15 +28,17 @@ class Modex
|
|
51
28
|
@mode = mode
|
52
29
|
@locked << thread
|
53
30
|
true
|
31
|
+
else
|
32
|
+
false
|
54
33
|
end
|
55
34
|
end
|
56
35
|
end
|
57
36
|
|
58
37
|
# the block is executed in the exclusive context
|
59
38
|
def lock mode
|
60
|
-
|
39
|
+
@m.synchronize do
|
61
40
|
thread = Thread.current
|
62
|
-
raise ThreadError if @locked.include?(thread)
|
41
|
+
raise ThreadError, "nesting not allowed" if @locked.include?(thread)
|
63
42
|
|
64
43
|
if @mode == mode and mode == SH and @waiting.empty? # strict queue
|
65
44
|
@locked << thread
|
@@ -68,8 +47,9 @@ class Modex
|
|
68
47
|
@locked << thread
|
69
48
|
else
|
70
49
|
@waiting << thread << mode
|
50
|
+
@m.unlock
|
71
51
|
Thread.stop
|
72
|
-
|
52
|
+
@m.lock
|
73
53
|
end
|
74
54
|
|
75
55
|
yield if block_given?
|
@@ -88,11 +68,20 @@ class Modex
|
|
88
68
|
|
89
69
|
# the block is executed in the exclusive context
|
90
70
|
def unlock
|
91
|
-
raise ThreadError unless @mode
|
71
|
+
raise ThreadError, "already unlocked" unless @mode
|
92
72
|
|
93
|
-
|
94
|
-
|
95
|
-
|
73
|
+
@m.synchronize do
|
74
|
+
if block_given?
|
75
|
+
begin
|
76
|
+
yield
|
77
|
+
ensure
|
78
|
+
@locked.delete Thread.current
|
79
|
+
end
|
80
|
+
|
81
|
+
else
|
82
|
+
@locked.delete Thread.current
|
83
|
+
end
|
84
|
+
|
96
85
|
wake_next_waiter if @locked.empty?
|
97
86
|
end
|
98
87
|
|
@@ -109,7 +98,7 @@ class Modex
|
|
109
98
|
@mode = EX
|
110
99
|
end
|
111
100
|
|
112
|
-
|
101
|
+
nonexclusive { do_when_first[arg] }
|
113
102
|
|
114
103
|
if mode == SH
|
115
104
|
@mode = SH
|
@@ -126,7 +115,7 @@ class Modex
|
|
126
115
|
if @locked.size == 1
|
127
116
|
if do_when_last
|
128
117
|
@mode = EX
|
129
|
-
|
118
|
+
nonexclusive { do_when_last[arg] }
|
130
119
|
end
|
131
120
|
@first = true
|
132
121
|
end
|
@@ -134,7 +123,7 @@ class Modex
|
|
134
123
|
end
|
135
124
|
|
136
125
|
def remove_dead # :nodoc:
|
137
|
-
|
126
|
+
@m.synchronize do
|
138
127
|
waiting = @waiting; @waiting = []
|
139
128
|
until waiting.empty?
|
140
129
|
|
@@ -148,13 +137,21 @@ class Modex
|
|
148
137
|
end
|
149
138
|
|
150
139
|
private
|
140
|
+
def nonexclusive
|
141
|
+
raise ThreadError unless @m.locked?
|
142
|
+
@m.unlock
|
143
|
+
yield
|
144
|
+
ensure
|
145
|
+
@m.lock
|
146
|
+
end
|
147
|
+
|
151
148
|
def wake_next_waiter
|
152
|
-
|
153
|
-
if
|
154
|
-
|
155
|
-
@locked <<
|
149
|
+
first_waiter = @waiting.shift; @mode = @waiting.shift && EX
|
150
|
+
if first_waiter
|
151
|
+
first_waiter.wakeup
|
152
|
+
@locked << first_waiter
|
156
153
|
end
|
157
|
-
|
154
|
+
first_waiter
|
158
155
|
rescue ThreadError
|
159
156
|
retry
|
160
157
|
end
|
data/test/test-concurrency.rb
CHANGED
@@ -295,7 +295,7 @@ class ConcurrencyTest
|
|
295
295
|
|
296
296
|
@total_iters = process_count * thread_count * rep_count
|
297
297
|
|
298
|
-
unless quiet or not $
|
298
|
+
unless quiet or not $stdout.isatty
|
299
299
|
monitor_thread = Thread.new do
|
300
300
|
loop do
|
301
301
|
print "\r#{status_meter}"
|
@@ -356,7 +356,7 @@ class ConcurrencyTest
|
|
356
356
|
|
357
357
|
def cleanup dir
|
358
358
|
@db.browse_dir dir do |child_path|
|
359
|
-
if
|
359
|
+
if child_path =~ /\/$/ ## ugh!
|
360
360
|
cleanup(child_path)
|
361
361
|
else
|
362
362
|
@db.delete(child_path)
|
@@ -68,10 +68,6 @@ class ConcurrencyTest
|
|
68
68
|
require 'profile'
|
69
69
|
end
|
70
70
|
|
71
|
-
opts.on("--fcntl-lock", "use fcntl version of flock") do
|
72
|
-
# actually, this arg is already picked out by test.rb
|
73
|
-
end
|
74
|
-
|
75
71
|
opts.on("--nofork", "do not use fork, even if available") do
|
76
72
|
params["nofork"] = true
|
77
73
|
end
|
data/test/test-formats.rb
CHANGED
@@ -32,7 +32,7 @@ class Test_Formats < Test::Unit::TestCase
|
|
32
32
|
# this is like in test-concurrency.rb -- generalize?
|
33
33
|
def cleanup dir
|
34
34
|
@db.browse_dir dir do |child_path|
|
35
|
-
if
|
35
|
+
if child_path =~ /\/$/ ## ugh!
|
36
36
|
cleanup(child_path)
|
37
37
|
else
|
38
38
|
@db.delete(child_path)
|
data/test/test-fsdb.rb
CHANGED
@@ -23,13 +23,15 @@ class Test_FSDB < Test::Unit::TestCase
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def test_zzz_cleanup # Argh! Test::Unit is missing a few features....
|
26
|
+
@db['foo.txt'] = "abc" # something to clean up
|
27
|
+
## really, cleanup should work when the dir is not there
|
26
28
|
cleanup '/'
|
27
29
|
end
|
28
30
|
|
29
31
|
# this is like in test-concurrency.rb -- generalize?
|
30
32
|
def cleanup dir
|
31
33
|
@db.browse_dir dir do |child_path|
|
32
|
-
if child_path
|
34
|
+
if child_path =~ /\/$/ ## ugh!
|
33
35
|
cleanup(child_path)
|
34
36
|
else
|
35
37
|
@db.delete(child_path)
|
data/test/test-modex.rb
CHANGED
@@ -4,28 +4,70 @@ require 'fsdb/modex'
|
|
4
4
|
|
5
5
|
include FSDB
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
DEBUG_MODEX = ARGV.delete '-d'
|
8
|
+
VERBOSE_MODEX = ARGV.delete '-v'
|
9
|
+
|
9
10
|
thread_count = (ARGV.shift || 10).to_i
|
10
11
|
rep_count = (ARGV.shift || 1000).to_i
|
11
12
|
|
13
|
+
def tabbed a
|
14
|
+
a.map {|s| s.sub(/^(\d+)/) {|n| " " * n.to_i * 20 + n}}
|
15
|
+
end
|
16
|
+
|
17
|
+
def thread_pass
|
18
|
+
if rand < 0.1
|
19
|
+
sleep rand/1000
|
20
|
+
else
|
21
|
+
Thread.pass
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
modex = Modex.new
|
26
|
+
counter = 0
|
27
|
+
|
12
28
|
out = []
|
13
29
|
sharers = 0
|
14
30
|
excluders = 0
|
15
31
|
dumper = proc do |str|
|
16
32
|
puts "*** #{str} called when there were #{sharers} threads sharing modex ***"
|
17
|
-
puts out[-20..-1]
|
33
|
+
puts tabbed(out[-20..-1])
|
18
34
|
exit!
|
19
35
|
end
|
20
36
|
|
37
|
+
class TestThread < Thread
|
38
|
+
def initialize n, outdev
|
39
|
+
self[:id] = n
|
40
|
+
self[:writes] = 0
|
41
|
+
@outdev = outdev
|
42
|
+
|
43
|
+
super do
|
44
|
+
begin
|
45
|
+
yield
|
46
|
+
rescue => ex
|
47
|
+
puts "#{ex}\n #{ex.backtrace.join("\n ")}"
|
48
|
+
# let thread stop, but let process continue
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#<test thread #{self[:id]}>"
|
55
|
+
end
|
56
|
+
|
57
|
+
if DEBUG_MODEX
|
58
|
+
def out s
|
59
|
+
@outdev << "#{self[:id]}: #{s}"
|
60
|
+
end
|
61
|
+
else
|
62
|
+
def out(*); end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
21
66
|
threads = (0...thread_count).map do |n|
|
22
|
-
|
67
|
+
TestThread.new(n, out) do
|
23
68
|
thread = Thread.current
|
24
|
-
|
25
|
-
thread[:writes] = 0
|
26
|
-
|
69
|
+
|
27
70
|
do_when_first = proc do
|
28
|
-
out << "#{thread[:id]}: do_when_first"
|
29
71
|
sharers += 1
|
30
72
|
if sharers > 1
|
31
73
|
dumper["do_when_first"]
|
@@ -33,7 +75,6 @@ threads = (0...thread_count).map do |n|
|
|
33
75
|
end
|
34
76
|
|
35
77
|
do_when_last = proc do
|
36
|
-
out << "#{thread[:id]}: do_when_last"
|
37
78
|
if sharers > 1
|
38
79
|
dumper["do_when_last"]
|
39
80
|
end
|
@@ -44,37 +85,40 @@ threads = (0...thread_count).map do |n|
|
|
44
85
|
x = rand(100)
|
45
86
|
case
|
46
87
|
when x < 50
|
47
|
-
out
|
88
|
+
thread.out "trying SH"
|
48
89
|
modex.synchronize(Modex::SH, do_when_first, do_when_last) do
|
49
|
-
out
|
90
|
+
thread.out "begin SH"
|
50
91
|
c_old = counter
|
51
|
-
|
92
|
+
thread_pass
|
52
93
|
raise if excluders > 0
|
53
94
|
raise unless counter == c_old
|
54
|
-
out
|
95
|
+
thread.out "end SH"
|
55
96
|
end
|
97
|
+
thread_pass
|
98
|
+
|
56
99
|
else
|
57
|
-
out
|
100
|
+
thread.out "trying EX"
|
58
101
|
modex.synchronize(Modex::EX) do
|
59
|
-
out
|
102
|
+
thread.out "begin EX"
|
60
103
|
excluders += 1
|
61
104
|
counter += 1
|
62
|
-
|
105
|
+
thread_pass
|
63
106
|
raise unless excluders == 1
|
107
|
+
raise unless sharers == 0
|
64
108
|
excluders -= 1
|
65
|
-
out
|
109
|
+
thread.out "end EX"
|
66
110
|
end
|
111
|
+
thread_pass
|
67
112
|
thread[:writes] += 1
|
68
113
|
end
|
69
114
|
end
|
70
115
|
end
|
71
116
|
end
|
72
117
|
|
73
|
-
expected = 0
|
74
|
-
|
118
|
+
expected = threads.inject(0) {|s, t| t.join; s + t[:writes]}
|
119
|
+
puts tabbed(out) if VERBOSE_MODEX
|
75
120
|
|
76
121
|
actual = counter
|
77
|
-
|
78
122
|
if expected == actual
|
79
123
|
puts "Test passed: #{actual} write operations"
|
80
124
|
else
|