Testing multiprocess behaviour is difficult and requires a way to synchronize processes at specific execution points. This gem allows the parent process to control the behaviour of child processes using breakpoints. It was originally built for testing the behaviour of database transactions and locking mechanisms.
Add this line to your application's Gemfile:
gem 'fork_break'
And then execute:
$ bundle
Or install it yourself as:
$ gem install fork_break
A simple example
process = ForkBreak::Process.new do |breakpoints|
sleep(1)
breakpoints << :after_sleep_1
sleep(2)
breakpoints << :after_sleep_2
end
def time(&block)
before = Time.now
block.call
(Time.now - before).round
end
puts time { process.run_until(:after_sleep_1).wait } # => 1
puts time { process.run_until(:after_sleep_2).wait } # => 2
puts time { process.finish.wait } # => 0
You can also get access to the breakpoints by including ForkBreak::Breakpoints, allowing you to test existing classes with minor changes. The following test the behaviour of using a file as a counter, with and without file locks.
class FileCounter
include ForkBreak::Breakpoints
def self.open(path, use_lock = true)
file = File.open(path, File::RDWR|File::CREAT, 0600)
return new(file, use_lock)
end
def initialize(file, use_lock = true)
@file = file
@use_lock = use_lock
end
def increase
breakpoints << :before_lock
@file.flock(File::LOCK_EX) if @use_lock
value = @file.read.to_i + 1
breakpoints << :after_read
@file.rewind
@file.write("#{value}\n")
@file.flush
@file.truncate(@file.pos)
end
end
def counter_after_synced_execution(counter_path, with_lock)
process1, process2 = 2.times.map do
ForkBreak::Process.new do
FileCounter.open(counter_path, with_lock).increase
end
end
process1.run_until(:after_read).wait
# process2 can't wait for read since it will block
process2.run_until(:before_lock).wait
process2.run_until(:after_read) && sleep(0.1)
process1.finish.wait # Finish process1
process2.finish.wait # Finish process2
File.read(counter_path).to_i
end
puts counter_after_synced_execution("counter_with_lock", true) # => 2
puts counter_after_synced_execution("counter_without_lock", false) # => 1
When running outside a ForkBreak process the breakpoints will be ignored so that you can use the same classes with breakpoints in production code.
There's also the possibility of adding a predefined timeout to the wait function and having it raise an exception.
process = ForkBreak::Process.new do
sleep(5)
end
process.finish.wait(timeout: 1) # will raise ForkBreak::WaitTimeout after 1 second
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
ForkBreak is released under the MIT License.