Skip to content

Fork with breakpoints, for testing multiprocess behaviour.

License

Notifications You must be signed in to change notification settings

Fatsoma/fork_break

 
 

Repository files navigation

ForkBreak

Build Status Dependency Status Gem Version Code Climate

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.

Installation

Add this line to your application's Gemfile:

gem 'fork_break'

And then execute:

$ bundle

Or install it yourself as:

$ gem install fork_break

Usage

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

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

License

ForkBreak is released under the MIT License.

About

Fork with breakpoints, for testing multiprocess behaviour.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 100.0%