fork_break 0.1.2 → 0.1.3
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -0
- data/README.md +10 -0
- data/Rakefile +1 -1
- data/fork_break.gemspec +19 -16
- data/lib/fork_break/breakpoint_setter.rb +19 -0
- data/lib/fork_break/breakpoints.rb +7 -0
- data/lib/fork_break/process.rb +49 -0
- data/lib/fork_break/version.rb +3 -0
- data/lib/fork_break.rb +6 -70
- data/spec/fork_break_spec.rb +83 -81
- data/spec/spec_helper.rb +1 -7
- metadata +39 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b499c0a1fefcb573b97b0b927c0715efc4b570b7
|
4
|
+
data.tar.gz: 4245c08783830dd2819a342e9e02a6247641e110
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2ae727120864671c37f8422ff1e9a541829a6751e36cf02b7db2296735e29a1b82134ad5f4241763be90d5d967a1935333b21206fd9efad7d2ab62a9187e866
|
7
|
+
data.tar.gz: 58c7081315b18fa3d8296a67006c2dd0caeb229b787b2443ef67af6c6cfa8eedf31a57aedeb79314669a4218b5ffc1391d0e87e20e3862da76d88a20ab4fa7ef
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Documentation:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
LineLength:
|
5
|
+
Max: 120
|
6
|
+
|
7
|
+
Lint/RescueException:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
SignalException:
|
11
|
+
EnforcedStyle: only_raise
|
12
|
+
SupportedStyles:
|
13
|
+
- only_raise
|
14
|
+
- only_fail
|
15
|
+
- semantic
|
16
|
+
|
17
|
+
Metrics/AbcSize:
|
18
|
+
Max: 18
|
19
|
+
|
20
|
+
Metrics/MethodLength:
|
21
|
+
Max: 15
|
data/README.md
CHANGED
@@ -98,6 +98,16 @@ puts counter_after_synced_execution("counter_with_lock", true) # => 2
|
|
98
98
|
puts counter_after_synced_execution("counter_without_lock", false) # => 1
|
99
99
|
```
|
100
100
|
|
101
|
+
There's also the possibility of adding a predefined timeout to the wait function and having it raise an exception.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
process = ForkBreak::Process.new do
|
105
|
+
sleep(5)
|
106
|
+
end
|
107
|
+
|
108
|
+
process.finish.wait(timeout: 1) # will raise ForkBreak::WaitTimeout after 1 second
|
109
|
+
```
|
110
|
+
|
101
111
|
## Contributing
|
102
112
|
|
103
113
|
1. Fork it
|
data/Rakefile
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
|
-
require
|
2
|
+
require 'bundler/gem_tasks'
|
data/fork_break.gemspec
CHANGED
@@ -1,20 +1,23 @@
|
|
1
|
-
|
1
|
+
require_relative 'lib/fork_break/version'
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
|
-
gem.authors = [
|
5
|
-
gem.email = [
|
6
|
-
gem.summary =
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
gem.
|
4
|
+
gem.authors = ['Petter Remen']
|
5
|
+
gem.email = ['petter.remen@gmail.com']
|
6
|
+
gem.summary =
|
7
|
+
'Testing multiprocess behaviour is difficult and requires a way to synchronize processes at specific execution ' \
|
8
|
+
'points. This gem allows the parent process to control the behaviour of child processes using breakpoints. It was' \
|
9
|
+
'originally built for testing the behaviour of database transactions and locking mechanisms.'
|
10
|
+
gem.description = 'Fork with breakpoints for syncing child process execution'
|
11
|
+
gem.homepage = 'http://github.com/remen/fork_break'
|
12
|
+
gem.licenses = ['MIT']
|
11
13
|
|
12
|
-
gem.files = `git ls-files`.split(
|
13
|
-
gem.executables = gem.files.grep(
|
14
|
-
gem.test_files = gem.files.grep(
|
15
|
-
gem.name =
|
16
|
-
gem.require_paths = [
|
17
|
-
gem.version =
|
18
|
-
gem.add_dependency
|
19
|
-
gem.add_development_dependency
|
14
|
+
gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
15
|
+
gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(/^(test|spec|features)\//)
|
17
|
+
gem.name = 'fork_break'
|
18
|
+
gem.require_paths = ['lib']
|
19
|
+
gem.version = ForkBreak::VERSION
|
20
|
+
gem.add_dependency 'fork', '= 1.0.1'
|
21
|
+
gem.add_development_dependency 'rspec', '~> 3.1', '>= 3.1.0'
|
22
|
+
gem.add_development_dependency 'rubocop', '~> 0.27', '>= 0.27.1'
|
20
23
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ForkBreak
|
2
|
+
class BreakpointSetter
|
3
|
+
def initialize(fork, debug = false)
|
4
|
+
@fork = fork
|
5
|
+
@next_breakpoint = :forkbreak_start
|
6
|
+
@debug = debug
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(symbol)
|
10
|
+
@fork.send_object(symbol)
|
11
|
+
if symbol == @next_breakpoint
|
12
|
+
@next_breakpoint = @fork.receive_object unless symbol == :forkbreak_end
|
13
|
+
puts "#{@fork.pid} received #{@next_breakpoint}" if @debug
|
14
|
+
end
|
15
|
+
rescue EOFError => exception
|
16
|
+
raise @fork.exception || exception
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ForkBreak
|
2
|
+
class Process
|
3
|
+
class << self
|
4
|
+
attr_accessor :breakpoint_setter
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(debug = false, &block)
|
8
|
+
@debug = debug
|
9
|
+
@fork = Fork.new(:return, :to_fork, :from_fork) do |child_fork|
|
10
|
+
self.class.breakpoint_setter = breakpoints = BreakpointSetter.new(child_fork, debug)
|
11
|
+
|
12
|
+
breakpoints << :forkbreak_start
|
13
|
+
block.call(breakpoints)
|
14
|
+
breakpoints << :forkbreak_end
|
15
|
+
|
16
|
+
self.class.breakpoint_setter = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_until(breakpoint)
|
21
|
+
@next_breakpoint = breakpoint
|
22
|
+
@fork.execute unless @fork.pid
|
23
|
+
puts "Parent is sending object #{breakpoint} to #{@fork.pid}" if @debug
|
24
|
+
@fork.send_object(breakpoint)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def wait(options = {})
|
29
|
+
# A timeout value of nil will execute the block without any timeout
|
30
|
+
Timeout.timeout(options[:timeout], WaitTimeout) do
|
31
|
+
loop do
|
32
|
+
brk = @fork.receive_object
|
33
|
+
puts "Parent is receiving object #{brk} from #{@fork.pid}" if @debug
|
34
|
+
if brk == @next_breakpoint
|
35
|
+
return self
|
36
|
+
elsif brk == :forkbreak_end
|
37
|
+
raise BreakpointNotReachedError, "Never reached breakpoint #{@next_breakpoint.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue EOFError => exception
|
42
|
+
raise @fork.exception || exception
|
43
|
+
end
|
44
|
+
|
45
|
+
def finish
|
46
|
+
run_until(:forkbreak_end)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/fork_break.rb
CHANGED
@@ -1,74 +1,10 @@
|
|
1
1
|
require 'fork'
|
2
|
+
require 'fork_break/breakpoint_setter'
|
3
|
+
require 'fork_break/breakpoints'
|
4
|
+
require 'fork_break/process'
|
5
|
+
require 'timeout'
|
2
6
|
|
3
7
|
module ForkBreak
|
4
|
-
class BreakpointNotReachedError < StandardError
|
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
|
-
rescue EOFError => exception
|
49
|
-
raise @fork.exception || exception
|
50
|
-
end
|
51
|
-
|
52
|
-
def finish
|
53
|
-
run_until(:forkbreak_end)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
class BreakpointSetter
|
58
|
-
def initialize(fork, debug = false)
|
59
|
-
@fork = fork
|
60
|
-
@next_breakpoint = :forkbreak_start
|
61
|
-
@debug = debug
|
62
|
-
end
|
63
|
-
|
64
|
-
def <<(symbol)
|
65
|
-
@fork.send_object(symbol)
|
66
|
-
if symbol == @next_breakpoint
|
67
|
-
@next_breakpoint = @fork.receive_object unless symbol == :forkbreak_end
|
68
|
-
puts "#{@fork.pid} received #{@next_breakpoint}" if @debug
|
69
|
-
end
|
70
|
-
rescue EOFError => exception
|
71
|
-
raise @fork.exception || exception
|
72
|
-
end
|
73
|
-
end
|
8
|
+
class BreakpointNotReachedError < StandardError; end
|
9
|
+
class WaitTimeout < StandardError; end
|
74
10
|
end
|
data/spec/fork_break_spec.rb
CHANGED
@@ -1,104 +1,106 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'tmpdir'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
FileUtils.touch(second_file)
|
14
|
-
end
|
15
|
-
expect(File.exists?(first_file)).to be_falsey
|
16
|
-
expect(File.exists?(second_file)).to be_falsey
|
17
|
-
|
18
|
-
process.run_until(:after_first_file).wait
|
19
|
-
expect(File.exists?(first_file)).to be_truthy
|
20
|
-
expect(File.exists?(second_file)).to be_falsey
|
21
|
-
|
22
|
-
process.finish.wait
|
23
|
-
expect(File.exists?(first_file)).to be_truthy
|
24
|
-
expect(File.exists?(second_file)).to be_truthy
|
4
|
+
describe ForkBreak::Process do
|
5
|
+
it 'works as intented' do
|
6
|
+
Dir.mktmpdir do |tmpdir|
|
7
|
+
first_file = File.join(tmpdir, 'first_file')
|
8
|
+
second_file = File.join(tmpdir, 'second_file')
|
9
|
+
process = ForkBreak::Process.new do |breakpoints|
|
10
|
+
FileUtils.touch(first_file)
|
11
|
+
breakpoints << :after_first_file
|
12
|
+
FileUtils.touch(second_file)
|
25
13
|
end
|
14
|
+
expect(File.exist?(first_file)).to be_falsey
|
15
|
+
expect(File.exist?(second_file)).to be_falsey
|
16
|
+
|
17
|
+
process.run_until(:after_first_file).wait
|
18
|
+
expect(File.exist?(first_file)).to be_truthy
|
19
|
+
expect(File.exist?(second_file)).to be_falsey
|
20
|
+
|
21
|
+
process.finish.wait
|
22
|
+
expect(File.exist?(first_file)).to be_truthy
|
23
|
+
expect(File.exist?(second_file)).to be_truthy
|
26
24
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
34
|
-
expect do
|
35
|
-
foo.run_until(:will_not_run).wait
|
36
|
-
end.to raise_error(BreakpointNotReachedError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'raises an error (on wait) if a breakpoint is not encountered' do
|
28
|
+
foo = ForkBreak::Process.new do |breakpoints|
|
29
|
+
breakpoints << :will_not_run if false # rubocop:disable LiteralInCondition
|
37
30
|
end
|
31
|
+
expect do
|
32
|
+
foo.run_until(:will_not_run).wait
|
33
|
+
end.to raise_error(ForkBreak::BreakpointNotReachedError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'works for the documentation example' do
|
37
|
+
class FileCounter
|
38
|
+
include ForkBreak::Breakpoints
|
39
|
+
|
40
|
+
def self.open(path, use_lock = true)
|
41
|
+
file = File.open(path, File::RDWR | File::CREAT, 0600)
|
42
|
+
new(file, use_lock)
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(file, use_lock = true)
|
46
|
+
@file = file
|
47
|
+
@use_lock = use_lock
|
48
|
+
end
|
38
49
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
50
|
+
def increase
|
51
|
+
breakpoints << :before_lock
|
52
|
+
@file.flock(File::LOCK_EX) if @use_lock
|
53
|
+
value = @file.read.to_i + 1
|
54
|
+
breakpoints << :after_read
|
55
|
+
@file.rewind
|
56
|
+
@file.write("#{value}\n")
|
57
|
+
@file.flush
|
58
|
+
@file.truncate(@file.pos)
|
63
59
|
end
|
60
|
+
end
|
64
61
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
end
|
62
|
+
def counter_after_synced_execution(counter_path, with_lock) # rubocop:disable Metrics/AbcSize
|
63
|
+
process1, process2 = 2.times.map do
|
64
|
+
ForkBreak::Process.new { FileCounter.open(counter_path, with_lock).increase }
|
65
|
+
end
|
71
66
|
|
72
|
-
|
67
|
+
process1.run_until(:after_read).wait
|
73
68
|
|
74
|
-
|
75
|
-
|
76
|
-
|
69
|
+
# process2 can't wait for read since it will block
|
70
|
+
process2.run_until(:before_lock).wait
|
71
|
+
process2.run_until(:after_read) && sleep(0.1)
|
77
72
|
|
78
|
-
|
79
|
-
|
73
|
+
process1.finish.wait # Finish process1
|
74
|
+
process2.finish.wait # Finish process2
|
80
75
|
|
81
|
-
|
82
|
-
|
76
|
+
File.read(counter_path).to_i
|
77
|
+
end
|
83
78
|
|
84
|
-
|
85
|
-
|
79
|
+
Dir.mktmpdir do |tmpdir|
|
80
|
+
counter_path = File.join(tmpdir, 'counter')
|
86
81
|
|
87
|
-
|
82
|
+
expect(counter_after_synced_execution(counter_path, true)).to eq(2)
|
88
83
|
|
89
|
-
|
90
|
-
|
91
|
-
end
|
84
|
+
File.unlink(counter_path)
|
85
|
+
expect(counter_after_synced_execution(counter_path, false)).to eq(1)
|
92
86
|
end
|
87
|
+
end
|
93
88
|
|
94
|
-
|
95
|
-
|
89
|
+
it 'raises the process exception' do
|
90
|
+
class MyException < StandardError; end
|
96
91
|
|
97
|
-
|
98
|
-
|
99
|
-
|
92
|
+
process = ForkBreak::Process.new do
|
93
|
+
raise MyException
|
94
|
+
end
|
100
95
|
|
101
|
-
|
96
|
+
expect { process.finish.wait }.to raise_error(MyException)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'raises a wait timeout eror when the process takes longer than the specified wait timeout' do
|
100
|
+
process = ForkBreak::Process.new do
|
101
|
+
sleep(1)
|
102
102
|
end
|
103
|
+
|
104
|
+
expect { process.finish.wait(timeout: 0.01) }.to raise_error(ForkBreak::WaitTimeout)
|
103
105
|
end
|
104
106
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,12 +1,6 @@
|
|
1
|
-
|
2
|
-
require 'bundler/setup'
|
3
|
-
require 'fork_break' # and any other gems you need
|
1
|
+
require_relative '../lib/fork_break'
|
4
2
|
|
5
3
|
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
4
|
# Run specs in random order to surface order dependencies. If you find an
|
11
5
|
# order dependency and want to debug it, you can fix the order by providing
|
12
6
|
# the seed, which is printed after each run.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fork_break
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Petter Remen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fork
|
@@ -28,16 +28,42 @@ dependencies:
|
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.1'
|
34
|
+
- - ">="
|
32
35
|
- !ruby/object:Gem::Version
|
33
36
|
version: 3.1.0
|
34
37
|
type: :development
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
38
|
-
- -
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.1'
|
44
|
+
- - ">="
|
39
45
|
- !ruby/object:Gem::Version
|
40
46
|
version: 3.1.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubocop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.27'
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: 0.27.1
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0.27'
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 0.27.1
|
41
67
|
description: Fork with breakpoints for syncing child process execution
|
42
68
|
email:
|
43
69
|
- petter.remen@gmail.com
|
@@ -47,16 +73,22 @@ extra_rdoc_files: []
|
|
47
73
|
files:
|
48
74
|
- ".gitignore"
|
49
75
|
- ".rspec"
|
76
|
+
- ".rubocop.yml"
|
50
77
|
- Gemfile
|
51
78
|
- LICENSE
|
52
79
|
- README.md
|
53
80
|
- Rakefile
|
54
81
|
- fork_break.gemspec
|
55
82
|
- lib/fork_break.rb
|
83
|
+
- lib/fork_break/breakpoint_setter.rb
|
84
|
+
- lib/fork_break/breakpoints.rb
|
85
|
+
- lib/fork_break/process.rb
|
86
|
+
- lib/fork_break/version.rb
|
56
87
|
- spec/fork_break_spec.rb
|
57
88
|
- spec/spec_helper.rb
|
58
89
|
homepage: http://github.com/remen/fork_break
|
59
|
-
licenses:
|
90
|
+
licenses:
|
91
|
+
- MIT
|
60
92
|
metadata: {}
|
61
93
|
post_install_message:
|
62
94
|
rdoc_options: []
|
@@ -79,8 +111,8 @@ signing_key:
|
|
79
111
|
specification_version: 4
|
80
112
|
summary: Testing multiprocess behaviour is difficult and requires a way to synchronize
|
81
113
|
processes at specific execution points. This gem allows the parent process to control
|
82
|
-
the behaviour of child processes using breakpoints. It
|
83
|
-
|
114
|
+
the behaviour of child processes using breakpoints. It wasoriginally built for testing
|
115
|
+
the behaviour of database transactions and locking mechanisms.
|
84
116
|
test_files:
|
85
117
|
- spec/fork_break_spec.rb
|
86
118
|
- spec/spec_helper.rb
|