object-channel 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.
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