object-channel 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in object-channel.gemspec
4
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2012 [Simply Measured](http://simplymeasured.com/)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ Object Channel
2
+ ==============
3
+
4
+ In some testing scenarios (such as signal handling), it is helpful to be able to test
5
+ from outside a process; common methods for communicating between two ends of a fork
6
+ include pushing data to an intermediary, such as a database or filesystem, but both
7
+ have side-effects: additional dependencies (many of which have their own dependencies)
8
+ and/or the need for post-run cleanup.
9
+
10
+ To avoid this, I often use IO.pipe:
11
+
12
+ ```ruby
13
+ reader,writer = IO.pipe
14
+
15
+ child_pid = fork do
16
+ reader.close
17
+ writer.write( "Hello, I'm #{Process.pid}" )
18
+ writer.close
19
+ end
20
+ writer.close
21
+
22
+ message_from_child = reader.read
23
+ reader.close
24
+
25
+ puts Process.pid # => 12338
26
+ puts child_pid # => 12345
27
+ puts message_from_child # => "Hello, I'm 12345"
28
+ ```
29
+
30
+ But that has several drawbacks:
31
+
32
+ - `read` only works if all `write` ends of the pipe are
33
+ closed; keeping track of which is open and which is closed
34
+ is a pain, especially if you have to fork several layers deep.
35
+ - only a single message can be passed; multiple messages
36
+ adds complexity, and complicated test cases aren't a good
37
+ idea
38
+ - only strings can be passed; sure Objects can be serialized, but...
39
+
40
+
41
+ A Simpler Way
42
+ ------------
43
+
44
+ Adding this kind of logic directly into your tests is a bad idea; it makes them
45
+ unnecessarily complicated, hard to read, and easy to get wrong. `ObjectChannel`
46
+ abstracts all that away so you can just test. Here's the same simple example, but
47
+ this time without all the pipe-closing:
48
+
49
+ ```ruby
50
+ require 'object-channel'
51
+
52
+ p2c = ObjectChannel.fork do |c2p|
53
+ c2p.transmit( "Hello, I'm #{Process.pid}" )
54
+ end
55
+
56
+ puts Process.pid # => 12338
57
+ puts p2c.pid # => 12345
58
+ puts p2c.receive! # => "Hello, I'm 12345"
59
+ ```
60
+
61
+ Features
62
+ --------
63
+
64
+ `ObjectChannel` provides the following features:
65
+
66
+ - Full-fledged objects, not just strings
67
+ - Bi-directional channel
68
+ - Blocking and non-blocking access
69
+ - block-invoke supported and fully optional
70
+ - No additional dependencies
71
+ - No cleanup necessary
72
+
73
+ Check out the [specs][] for examples.
74
+
75
+ License & Contributing
76
+ ======================
77
+
78
+ Project is (c) 2012 [Simply Measured][simply-measured] and released under an
79
+ [MIT-style license][license]
80
+
81
+ * This project uses Vincent Driessen's [Git-Flow][git-flow] branching model.
82
+ * Check out the latest develop to make sure the feature hasn't been implemented
83
+ or the bug hasn't been fixed yet
84
+ * Check out the issue tracker to make sure someone already hasn't requested it
85
+ and/or contributed it
86
+ * Fork the project
87
+ * Start a feature/bugfix branch
88
+ * Commit and push until you are happy with your contribution
89
+ * Make sure to add tests for it. This is important so I don't break it in a
90
+ future version unintentionally.
91
+ * Please try not to mess with the Rakefile, version, or history. If you want to
92
+ have your own version, or is otherwise necessary, that is fine, but please
93
+ isolate to its own commit so I can cherry-pick around it.
94
+
95
+
96
+ <!-- INTERNAL LINKS -->
97
+
98
+ [license]: https://github.com/simplymeasured/object-channel/blob/master/LICENSE.md
99
+ [specs]: https://github.com/simplymeasured/object-channel/blob/master/spec/
100
+
101
+ <!-- EXTERNAL LINKS -->
102
+
103
+ [simply-measured]: http://simplymeasured.com/
104
+ [git-flow]: http://nvie.com/posts/a-successful-git-branching-model/
105
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = FileList['spec/**/*_spec.rb']
7
+ end
@@ -0,0 +1,228 @@
1
+ # require "object-channel/version"
2
+
3
+ class ObjectChannel
4
+ class << self
5
+ ##
6
+ # Create object channels endpoints, each connected by an object channel.
7
+ #
8
+ def create(endpoints=2)
9
+ endpoints.times.to_a.map do |index|
10
+ pipe = IO.pipe
11
+ {:index=>index, :pipe=>pipe}
12
+ end.permutation(2).to_a.inject(Hash.new{|h,k|h[k]=Hash.new}) do |channels,pipes|
13
+ from = pipes[0]
14
+ to = pipes[1]
15
+ channels[from[:index]][to[:index]] = ObjectChannel.new( from[:pipe], to[:pipe] )
16
+ channels
17
+ end
18
+ end
19
+
20
+ # :nodoc:
21
+ def new(receive_pipe,transmit_pipe)
22
+ object_channel = allocate
23
+ [
24
+ :@receive_reader,
25
+ :@receive_writer,
26
+ :@transmit_reader,
27
+ :@transmit_writer
28
+ ].zip([receive_pipe,transmit_pipe].flatten(1)) do |name,ios|
29
+ object_channel.instance_variable_set(name,ios)
30
+ end
31
+ object_channel
32
+ end
33
+
34
+ ##
35
+ # Creates a subprocess with a bi-directional ObjectChannel
36
+ # available to both processes, behaving otherwise like Process.fork.
37
+ #
38
+ # If a block is specified, the block is run in the subprocess with
39
+ # the ObjectChannel::Pair to its parent as its only argument. Like
40
+ # Process.fork(), the subprocess terminates with a status of zero.
41
+ #
42
+ # Otherwise, the fork call returns twice, once in the parent, returning
43
+ # the ObjectChannel to its child whose pid method is the process ID
44
+ # of that child, and once in the child, returning the ObjectChannel to
45
+ # the parent, whose pid method returns nil.
46
+ #
47
+ # :call-seq:
48
+ # ObjectChannel.fork -> ObjectChannel x 2
49
+ # ObjectChannel.fork{|channel|} -> ObjectChannel
50
+ #
51
+ # = Example (Block)
52
+ #
53
+ # p2c = ObjectChannel.fork do |c2p|
54
+ # c2p.transmit :foo => :bar
55
+ # puts "child received <#{p2c.receive!.inspect}>" #=> [1, 2, 3]
56
+ # end
57
+ # puts "parent received <#{p2c.receive!.inspect}>" #=> {:foo=>:bar}
58
+ # channel.transmit [1,2,3]
59
+ #
60
+ # = Example (No Block)
61
+ #
62
+ # if (channel = ObjectChannel.fork).pid
63
+ # channel.transmit :foo => :bar
64
+ # puts "child received <#{channel.receive!.inspect}>" #=> [1, 2, 3]
65
+ # else
66
+ # puts "received <#{channel.receive!.inspect}>" #=> {:foo=>:bar}
67
+ # channel.transmit [1,2,3]
68
+ # exit
69
+ # end
70
+ #
71
+ def fork name=nil, &block
72
+ channels = create(2)
73
+ channel_a = channels[0][1]
74
+ channel_b = channels[1][0]
75
+
76
+ if block_given?
77
+ ret = fork name
78
+ if ret.nil? or (ret.respond_to?(:pid) and ret.pid.nil?)
79
+ begin
80
+ block.call(*(ret.respond_to?(:pid) ? [ret] : [] ) )
81
+ ensure
82
+ exit!
83
+ end
84
+ end
85
+ return ret
86
+ else
87
+ pid = forker.fork
88
+ channel = ( pid.nil? ? channel_a : channel_b)
89
+ if name.nil?
90
+ channel.instance_variable_set(:@pid, pid)
91
+ channel
92
+ else
93
+ Object.const_set name, channel
94
+ pid
95
+ end
96
+ end
97
+ end
98
+
99
+ ##
100
+ # Allows the object that forks to be replaced, helpful for adding additional
101
+ # functionality in something of a daisy-chain.
102
+ #
103
+ # The forker's fork method *must* be API-compatible with Kernel.fork:
104
+ # - It *must* work with and without blocks provided
105
+ # - It *must* return an integer pid to the parent and nil to the child
106
+ # - It *must not* require any arguments, as args are swallowed in ObjectChannel.fork()
107
+ #
108
+ attr_writer :forker
109
+ def forker
110
+ (@forker || ::Kernel)
111
+ end
112
+ end
113
+
114
+ attr_reader :pid
115
+
116
+ ##
117
+ # Transmit an object
118
+ #
119
+ def transmit(object)
120
+ close(:@transmit_reader)
121
+ @transmit_writer.write [Marshal.dump(object)].pack('m').gsub("\n",'') + "\n"
122
+ end
123
+
124
+ ##
125
+ # Receive the next object in a blocking manner
126
+ #
127
+ def receive!
128
+ close(:@receive_writer)
129
+ begin
130
+ r = @receive_reader.readline
131
+ Marshal.load(r.unpack("m")[0])
132
+ rescue EOFError
133
+ return nil
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Receive the next object in a non-blocking manner
139
+ # Returns nil if no object was present
140
+ def receive
141
+ receive! if ready?
142
+ end
143
+
144
+ ##
145
+ # Test to see if there is anything to receive
146
+ #
147
+ def ready?
148
+ c = Thread.start{@receive_reader.getc}
149
+ Thread.pass
150
+ if c.status == false and c.value
151
+ @receive_reader.ungetc(c.value)
152
+ return true
153
+ else
154
+ c.kill
155
+ return false
156
+ end
157
+ end
158
+
159
+ ##
160
+ # Test to see if the receive_reader is at eof in a non-blocking manner
161
+ #
162
+ def closed?
163
+ c = Thread.start{@receive_reader.eof?}
164
+ Thread.pass
165
+ if c.status == false and c.value
166
+ return true
167
+ else
168
+ c.kill
169
+ return false
170
+ end
171
+ end
172
+
173
+ ##
174
+ # Close all ends of all IOS.
175
+ #
176
+ def close!
177
+ [
178
+ :@receive_reader,
179
+ :@receive_writer,
180
+ :@transmit_reader,
181
+ :@transmit_writer
182
+ ].each &method(:close)
183
+ end
184
+
185
+ ##
186
+ # Close an IO by name
187
+ #
188
+ def close(ios_name)
189
+ ios = instance_variable_get(ios_name)
190
+ ios.close unless ios.closed?
191
+ end
192
+ end
193
+
194
+ ##
195
+ # A Bonus Example:
196
+ #
197
+ # a proc that both call
198
+ # pingpong = Proc.new do |channel, action|
199
+ # srand(Process.pid)
200
+ # def report(message) puts %Q{#{Process.pid}: #{message}}; end
201
+ # begin
202
+ # break (report 'VICTORY!') if channel.closed? or !(received = channel.receive!)
203
+ # report "got #{received}"
204
+
205
+ # break (report 'aw, man!') if Kernel.rand(99) > 61
206
+ # report "returning <#{action}>"
207
+ # channel.transmit(action)
208
+ # end while true
209
+ # channel.close!
210
+ # end
211
+
212
+ # # without a block
213
+ # if (channel = ObjectChannel.fork).pid
214
+ # pingpong.call(channel, 'ping!')
215
+ # Process.wait(channel.pid)
216
+ # else
217
+ # channel.transmit('serve!')
218
+ # pingpong.call(channel, 'pong.')
219
+ # exit
220
+ # end
221
+
222
+ # # with a block
223
+ # channel = ObjectChannel.fork do |channel|
224
+ # pingpong.call(channel,'ping!')
225
+ # end
226
+ # channel.transmit 'SERVICE!'
227
+ # pingpong.call(channel,'pong.')
228
+ # Process.wait(channel.pid)
@@ -0,0 +1,3 @@
1
+ class ObjectChannel
2
+ VERSION = "0.1.3"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "object-channel/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "object-channel"
7
+ s.version = ObjectChannel::VERSION
8
+ s.authors = ["Ryan Biesemeyer"]
9
+ s.email = ["ryan@yaauie.com"]
10
+ s.homepage = "https://github.com/simplymeasured/object-channel"
11
+ s.summary = %q{A library for sending objects over pipes, with convenience methods for forking}
12
+ s.description = %q{Wraps around Process.fork(), providing parent and child each an ObjectChannel to transmit objects to and receive objects from the other.}
13
+
14
+ #s.rubyforge_project = "object-channel"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'rspec'
22
+ s.add_development_dependency 'rake'
23
+ s.add_development_dependency 'bundler'
24
+ end
@@ -0,0 +1,165 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe 'ObjectChannel' do
4
+ module CustomForker
5
+ extend self
6
+ def fork(*a,&b)
7
+ $FORKER = "#{self.to_s}.fork"
8
+ ::Kernel.fork(*a,&b)
9
+ end
10
+ end
11
+
12
+ [nil,CustomForker].each do |forker|
13
+ before(:each) do
14
+ ObjectChannel.forker = forker
15
+ end
16
+ after(:each) do
17
+ ObjectChannel.forker = forker
18
+ $FORKER = nil
19
+ end
20
+ describe '#fork' do
21
+ it 'should be able to send an object for processing' do
22
+ channel = ObjectChannel.fork do |channel|
23
+ received = channel.receive!
24
+ channel.transmit( received + 1 )
25
+ end
26
+ channel.transmit 100
27
+ channel.receive!.should == 101
28
+ end
29
+
30
+ it 'should actually happen on a separate process' do
31
+ def prepend_pid(s); "#{Process.pid}: #{s}"; end
32
+ channel = ObjectChannel.fork do |channel|
33
+ received = channel.receive!
34
+ channel.transmit prepend_pid(received)
35
+ end
36
+ channel.transmit 'foobar'
37
+ channel.receive!.should_not == prepend_pid('foobar')
38
+ end
39
+
40
+ context 'with constantized channels' do
41
+ after(:each){ Object.send(:remove_const, :NamedChannel) }
42
+ it 'should be able to use the constantized channels' do
43
+ ObjectChannel.fork :NamedChannel do
44
+ received = NamedChannel.receive!
45
+ NamedChannel.transmit received.reverse
46
+ end
47
+ NamedChannel.transmit 'foobar'
48
+ NamedChannel.receive!.should == 'raboof'
49
+ NamedChannel.close!
50
+ end
51
+ end
52
+
53
+ describe 'block-invoke' do
54
+ after(:each){ Object.send(:remove_const, :OuterChannel) }
55
+ # OK, I have to do some tricky magic here.
56
+ # If you know of a better way, by all means, reimplement.
57
+ it 'should invoke exit! when done' do
58
+ if (ObjectChannel.fork :OuterChannel)
59
+ begin
60
+ ObjectChannel.fork :InnerChannel do
61
+ InnerChannel.receive!
62
+ end
63
+ InnerChannel.transmit 'foo' #get the party started
64
+ rescue SystemExit => exception
65
+ OuterChannel.transmit exception
66
+ raise exception
67
+ end
68
+ exit! 0
69
+ else
70
+ output = OuterChannel.receive!
71
+ output.should be_nil
72
+ output.inspect.should_not == '#<SystemExit: exit>'
73
+ OuterChannel.receive!.should be_nil # it shouldn't invoke exit twice!
74
+ end
75
+ end
76
+
77
+ it 'should run the block in the child' do
78
+ cached_pid = Process.pid
79
+ ObjectChannel.fork(:OuterChannel){exit!}
80
+ Process.pid.should == cached_pid
81
+ end
82
+ end
83
+
84
+ unless forker.nil?
85
+ context 'with a custom forker' do
86
+ context 'ObjectChannel.forker' do
87
+ subject{ ObjectChannel.forker }
88
+ it{ should_not be_nil }
89
+ end
90
+ context 'when run' do
91
+ before(:each) do
92
+ ObjectChannel.fork(:NamedChannel){ $RAN=true }
93
+ end
94
+ after(:each){ Object.send(:remove_const, :NamedChannel) }
95
+ subject{ $FORKER }
96
+ it 'should have run in the custom forker' do
97
+ should == "#{forker.to_s}.fork"
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+
107
+ # Bonus example:
108
+ # An Isolater. It'll run the supplied block in a child process,
109
+ # then appropriately return or raise the result in the parent process.
110
+ # Note: It does not currently support catch/throw, so it's possible to bypass.
111
+ describe 'isolate' do
112
+ module Isolater
113
+ extend self
114
+ def isolate( &block )
115
+ p2c = ObjectChannel.fork do |c2p|
116
+ begin
117
+ c2p.transmit( [:returned, block.call] )
118
+ rescue Object => obj_raised
119
+ c2p.transmit( [:raised, obj_raised] )
120
+ end
121
+ end
122
+ verb, value = p2c.receive!
123
+
124
+ raise value if verb == :raised
125
+ return value if verb == :returned
126
+
127
+ raise RuntimeError, 'expected something to be returned or raised; got <#{verb},#{value}>'
128
+ end
129
+ def dirty?
130
+ !!(@dirty)
131
+ end
132
+ attr_writer :dirty
133
+ end
134
+
135
+ it 'should be isolated' do
136
+ Isolater.dirty = false
137
+ Isolater.isolate do
138
+ Isolater.dirty = true
139
+ end
140
+ Isolater.dirty?.should be_false
141
+ end
142
+
143
+ context 'when block returns' do
144
+ subject do
145
+ lambda{ return { :k=>'value', :int=>4 } }
146
+ end
147
+
148
+ it 'should not raise' do
149
+ expect{ Isolater.isolate{ subject.call } }.to_not raise_exception
150
+ end
151
+ it 'should return the correct value' do
152
+ subject.call.should == { :k=>'value', :int=>4 }
153
+ end
154
+ end
155
+
156
+ context 'when block raises' do
157
+ subject do
158
+ lambda{ $GLOBAL_VALUE = true; 5 / 0 }
159
+ end
160
+
161
+ it 'should raise' do
162
+ expect{ Isolater.isolate{ subject.call } }.to raise_exception(ZeroDivisionError)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'object-channel'
5
+
6
+
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: object-channel
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 3
10
+ version: 0.1.3
11
+ platform: ruby
12
+ authors:
13
+ - Ryan Biesemeyer
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-19 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rake
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: bundler
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: Wraps around Process.fork(), providing parent and child each an ObjectChannel to transmit objects to and receive objects from the other.
63
+ email:
64
+ - ryan@yaauie.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files: []
70
+
71
+ files:
72
+ - .gitignore
73
+ - Gemfile
74
+ - LICENSE.md
75
+ - README.md
76
+ - Rakefile
77
+ - lib/object-channel.rb
78
+ - lib/object-channel/version.rb
79
+ - object-channel.gemspec
80
+ - spec/object-channel_spec.rb
81
+ - spec/spec_helper.rb
82
+ homepage: https://github.com/simplymeasured/object-channel
83
+ licenses: []
84
+
85
+ post_install_message:
86
+ rdoc_options: []
87
+
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 3
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ requirements: []
109
+
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.10
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: A library for sending objects over pipes, with convenience methods for forking
115
+ test_files:
116
+ - spec/object-channel_spec.rb
117
+ - spec/spec_helper.rb