fork_break 0.1.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/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +109 -0
- data/Rakefile +2 -0
- data/fork_break.gemspec +20 -0
- data/lib/fork_break.rb +70 -0
- data/spec/fork_break_spec.rb +94 -0
- data/spec/spec_helper.rb +15 -0
- metadata +96 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Petter Remen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# ForkBreak
|
2
|
+
|
3
|
+
Fork with breakpoints, for testing multiprocess behaviour.
|
4
|
+
|
5
|
+
Testing multiprocess behaviour is difficult and requires a way to synchronize processes at
|
6
|
+
specific execution points. This gem allows the parent process to control the behaviour of child processes using
|
7
|
+
breakpoints. It was originally built for testing the behaviour of database transactions and locking mechanisms.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'fork_break'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install fork_break
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
A simple example
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
process = ForkBreak::Process.new do |breakpoints|
|
29
|
+
sleep(1)
|
30
|
+
breakpoints << :after_sleep_1
|
31
|
+
sleep(2)
|
32
|
+
breakpoints << :after_sleep_2
|
33
|
+
end
|
34
|
+
|
35
|
+
def time(&block)
|
36
|
+
before = Time.now
|
37
|
+
block.call
|
38
|
+
(Time.now - before).round
|
39
|
+
end
|
40
|
+
|
41
|
+
puts time { process.run_until(:after_sleep_1).wait } # => 1
|
42
|
+
puts time { process.run_until(:after_sleep_2).wait } # => 2
|
43
|
+
puts time { process.finish.wait } # => 0
|
44
|
+
```
|
45
|
+
|
46
|
+
You can also get access to the breakpoints by including ForkBreak::Breakpoints, allowing you to test
|
47
|
+
existing classes with minor changes. The following test the behaviour of using a file as a counter, with
|
48
|
+
and without file locks.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class FileCounter
|
52
|
+
include ForkBreak::Breakpoints
|
53
|
+
|
54
|
+
def self.open(path, use_lock = true)
|
55
|
+
file = File.open(path, File::RDWR|File::CREAT, 0600)
|
56
|
+
return new(file, use_lock)
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(file, use_lock = true)
|
60
|
+
@file = file
|
61
|
+
@use_lock = use_lock
|
62
|
+
end
|
63
|
+
|
64
|
+
def increase
|
65
|
+
|
66
|
+
breakpoints << :before_lock
|
67
|
+
|
68
|
+
@file.flock(File::LOCK_EX) if @use_lock
|
69
|
+
value = @file.read.to_i + 1
|
70
|
+
|
71
|
+
breakpoints << :after_read
|
72
|
+
|
73
|
+
@file.rewind
|
74
|
+
@file.write("#{value}\n")
|
75
|
+
@file.flush
|
76
|
+
@file.truncate(@file.pos)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def counter_after_synced_execution(counter_path, with_lock)
|
81
|
+
process1, process2 = 2.times.map do
|
82
|
+
ForkBreak::Process.new do
|
83
|
+
FileCounter.open(counter_path, with_lock).increase
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
process1.run_until(:after_read).wait
|
88
|
+
|
89
|
+
# process2 can't wait for read since it will block
|
90
|
+
process2.run_until(:before_lock).wait
|
91
|
+
process2.run_until(:after_read) && sleep(0.1)
|
92
|
+
|
93
|
+
process1.finish.wait # Finish process1
|
94
|
+
process2.finish.wait # Finish process2
|
95
|
+
|
96
|
+
File.read(counter_path).to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
puts counter_after_synced_execution("counter_with_lock", true) # => 2
|
100
|
+
puts counter_after_synced_execution("counter_without_lock", false) # => 1
|
101
|
+
```
|
102
|
+
|
103
|
+
## Contributing
|
104
|
+
|
105
|
+
1. Fork it
|
106
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
107
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
108
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
109
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/fork_break.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Petter Remen"]
|
5
|
+
gem.email = ["petter.remen@gmail.com"]
|
6
|
+
gem.description = %q{Testing multiprocess behaviour is difficult and requires a way to synchronize processes at
|
7
|
+
specific execution points. This gem allows the parent process to control the behaviour of child processes using
|
8
|
+
breakpoints. It was originally built for testing the behaviour of database transactions and locking mechanisms. }
|
9
|
+
gem.summary = %q{Fork with breakpoints for syncing child process execution}
|
10
|
+
gem.homepage = "http://github.com/remen/fork_break"
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "fork_break"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = "0.1.0"
|
18
|
+
gem.add_dependency "fork"
|
19
|
+
gem.add_development_dependency "rspec"
|
20
|
+
end
|
data/lib/fork_break.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'fork'
|
2
|
+
|
3
|
+
module ForkBreak
|
4
|
+
class BreakpointNotReachedError < StandardError ; end
|
5
|
+
|
6
|
+
module Breakpoints
|
7
|
+
def breakpoints
|
8
|
+
return ForkBreak::Process.breakpoint_setter
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Process
|
13
|
+
class << self
|
14
|
+
attr_accessor :breakpoint_setter
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(debug = false, &block)
|
18
|
+
@debug = debug
|
19
|
+
@fork = Fork.new(:return, :to_fork, :from_fork) do |child_fork|
|
20
|
+
self.class.breakpoint_setter = breakpoints = BreakpointSetter.new(child_fork, debug)
|
21
|
+
|
22
|
+
breakpoints << :forkbreak_start
|
23
|
+
block.call(breakpoints)
|
24
|
+
breakpoints << :forkbreak_end
|
25
|
+
|
26
|
+
self.class.breakpoint_setter = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_until(breakpoint)
|
31
|
+
@next_breakpoint = breakpoint
|
32
|
+
@fork.execute unless @fork.pid
|
33
|
+
puts "Parent is sending object #{breakpoint} to #{@fork.pid}" if @debug
|
34
|
+
@fork.send_object(breakpoint)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def wait
|
39
|
+
loop do
|
40
|
+
brk = @fork.receive_object
|
41
|
+
puts "Parent is receiving object #{brk} from #{@fork.pid}" if @debug
|
42
|
+
if brk == @next_breakpoint
|
43
|
+
return self
|
44
|
+
elsif brk == :forkbreak_end
|
45
|
+
raise BreakpointNotReachedError.new("Never reached breakpoint #{@next_breakpoint.inspect}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def finish
|
51
|
+
run_until(:forkbreak_end)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class BreakpointSetter
|
56
|
+
def initialize(fork, debug = false)
|
57
|
+
@fork = fork
|
58
|
+
@next_breakpoint = :forkbreak_start
|
59
|
+
@debug = debug
|
60
|
+
end
|
61
|
+
|
62
|
+
def <<(symbol)
|
63
|
+
@fork.send_object(symbol)
|
64
|
+
if symbol == @next_breakpoint
|
65
|
+
@next_breakpoint = @fork.receive_object unless symbol == :forkbreak_end
|
66
|
+
puts "#{@fork.pid} received #{@next_breakpoint}" if @debug
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
module ForkBreak
|
5
|
+
describe Process do
|
6
|
+
it "works as intented" do
|
7
|
+
Dir.mktmpdir do |tmpdir|
|
8
|
+
first_file = File.join(tmpdir, "first_file")
|
9
|
+
second_file = File.join(tmpdir, "second_file")
|
10
|
+
process = Process.new do |breakpoints|
|
11
|
+
FileUtils.touch(first_file)
|
12
|
+
breakpoints << :after_first_file
|
13
|
+
FileUtils.touch(second_file)
|
14
|
+
end
|
15
|
+
File.exists?(first_file).should be_false
|
16
|
+
File.exists?(second_file).should be_false
|
17
|
+
|
18
|
+
process.run_until(:after_first_file).wait
|
19
|
+
File.exists?(first_file).should be_true
|
20
|
+
File.exists?(second_file).should be_false
|
21
|
+
|
22
|
+
process.finish.wait
|
23
|
+
File.exists?(first_file).should be_true
|
24
|
+
File.exists?(second_file).should be_true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "raises an error (on wait) if a breakpoint is not encountered" do
|
29
|
+
foo = Process.new do |breakpoints|
|
30
|
+
if false
|
31
|
+
breakpoints << :will_not_run
|
32
|
+
end
|
33
|
+
end
|
34
|
+
expect do
|
35
|
+
foo.run_until(:will_not_run).wait
|
36
|
+
end.to raise_error(BreakpointNotReachedError)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "works for the documentation example" do
|
40
|
+
class FileCounter
|
41
|
+
include ForkBreak::Breakpoints
|
42
|
+
|
43
|
+
def self.open(path, use_lock = true)
|
44
|
+
file = File.open(path, File::RDWR|File::CREAT, 0600)
|
45
|
+
return new(file, use_lock)
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(file, use_lock = true)
|
49
|
+
@file = file
|
50
|
+
@use_lock = use_lock
|
51
|
+
end
|
52
|
+
|
53
|
+
def increase
|
54
|
+
breakpoints << :before_lock
|
55
|
+
@file.flock(File::LOCK_EX) if @use_lock
|
56
|
+
value = @file.read.to_i + 1
|
57
|
+
breakpoints << :after_read
|
58
|
+
@file.rewind
|
59
|
+
@file.write("#{value}\n")
|
60
|
+
@file.flush
|
61
|
+
@file.truncate(@file.pos)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def counter_after_synced_execution(counter_path, with_lock)
|
66
|
+
process1, process2 = 2.times.map do
|
67
|
+
ForkBreak::Process.new do
|
68
|
+
FileCounter.open(counter_path, with_lock).increase
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
process1.run_until(:after_read).wait
|
73
|
+
|
74
|
+
# process2 can't wait for read since it will block
|
75
|
+
process2.run_until(:before_lock).wait
|
76
|
+
process2.run_until(:after_read) && sleep(0.1)
|
77
|
+
|
78
|
+
process1.finish.wait # Finish process1
|
79
|
+
process2.finish.wait # Finish process2
|
80
|
+
|
81
|
+
File.read(counter_path).to_i
|
82
|
+
end
|
83
|
+
|
84
|
+
Dir.mktmpdir do |tmpdir|
|
85
|
+
counter_path = File.join(tmpdir, "counter")
|
86
|
+
|
87
|
+
counter_after_synced_execution(counter_path, with_lock = true).should == 2
|
88
|
+
|
89
|
+
File.unlink(counter_path)
|
90
|
+
counter_after_synced_execution(counter_path, with_lock = false).should == 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'fork_break' # and any other gems you need
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
7
|
+
config.run_all_when_everything_filtered = true
|
8
|
+
config.filter_run :focus
|
9
|
+
|
10
|
+
# Run specs in random order to surface order dependencies. If you find an
|
11
|
+
# order dependency and want to debug it, you can fix the order by providing
|
12
|
+
# the seed, which is printed after each run.
|
13
|
+
# --seed 1234
|
14
|
+
config.order = 'random'
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fork_break
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Petter Remen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: fork
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: ! 'Testing multiprocess behaviour is difficult and requires a way to
|
47
|
+
synchronize processes at
|
48
|
+
|
49
|
+
specific execution points. This gem allows the parent process to control the behaviour
|
50
|
+
of child processes using
|
51
|
+
|
52
|
+
breakpoints. It was originally built for testing the behaviour of database transactions
|
53
|
+
and locking mechanisms. '
|
54
|
+
email:
|
55
|
+
- petter.remen@gmail.com
|
56
|
+
executables: []
|
57
|
+
extensions: []
|
58
|
+
extra_rdoc_files: []
|
59
|
+
files:
|
60
|
+
- .gitignore
|
61
|
+
- .rspec
|
62
|
+
- Gemfile
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- fork_break.gemspec
|
67
|
+
- lib/fork_break.rb
|
68
|
+
- spec/fork_break_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: http://github.com/remen/fork_break
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.24
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Fork with breakpoints for syncing child process execution
|
94
|
+
test_files:
|
95
|
+
- spec/fork_break_spec.rb
|
96
|
+
- spec/spec_helper.rb
|