fork_break 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fork_break.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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
@@ -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