right_popen 1.0.21 → 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +10 -8
- data/lib/right_popen.rb +125 -30
- data/lib/right_popen/linux/accumulator.rb +14 -4
- data/lib/right_popen/linux/{right_popen.rb → popen3_async.rb} +73 -57
- data/lib/right_popen/linux/popen3_sync.rb +35 -0
- data/lib/right_popen/linux/process.rb +136 -44
- data/lib/right_popen/linux/utilities.rb +5 -2
- data/lib/right_popen/process_base.rb +339 -0
- data/lib/right_popen/process_status.rb +64 -0
- data/lib/right_popen/safe_output_buffer.rb +79 -0
- data/lib/right_popen/target_proxy.rb +67 -0
- data/lib/right_popen/version.rb +2 -2
- data/right_popen.gemspec +2 -2
- data/spec/produce_mixed_output.rb +3 -0
- data/spec/right_popen/linux/accumulator_spec.rb +5 -13
- data/spec/right_popen/safe_output_buffer_spec.rb +26 -0
- data/spec/right_popen_spec.rb +272 -227
- data/spec/runner.rb +171 -79
- data/spec/sleeper.rb +35 -0
- data/spec/stdout.rb +1 -1
- data/spec/writer.rb +34 -0
- metadata +32 -26
@@ -0,0 +1,64 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
|
26
|
+
module RightPopen
|
27
|
+
|
28
|
+
# Quacks like Process::Status, which we cannot instantiate ourselves because
|
29
|
+
# has no public new method for cases where we need to create our own.
|
30
|
+
class ProcessStatus
|
31
|
+
|
32
|
+
attr_reader :pid, :exitstatus, :termsig
|
33
|
+
|
34
|
+
# === Parameters
|
35
|
+
# @param [Integer] pid as process identifier
|
36
|
+
# @param [Integer] exitstatus as process exit code or nil
|
37
|
+
# @param [Integer] termination signal or nil
|
38
|
+
def initialize(pid, exitstatus, termsig=nil)
|
39
|
+
@pid = pid
|
40
|
+
@exitstatus = exitstatus
|
41
|
+
@termsig = termsig
|
42
|
+
end
|
43
|
+
|
44
|
+
# Simulates Process::Status.exited? which seems like a weird method since
|
45
|
+
# this object cannot logically be queried until the process exits.
|
46
|
+
#
|
47
|
+
# === Returns
|
48
|
+
# @return [TrueClass] always true
|
49
|
+
def exited?
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Simulates Process::Status.success?
|
54
|
+
#
|
55
|
+
# === Returns
|
56
|
+
# true if the process returned zero as its exit code or nil if terminate was signalled
|
57
|
+
def success?
|
58
|
+
# note that Linux ruby returns nil when exitstatus is nil and a termsig
|
59
|
+
# value is set instead.
|
60
|
+
return @exitstatus ? (0 == @exitstatus) : nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
|
26
|
+
module RightPopen
|
27
|
+
|
28
|
+
# Provides an output handler implementation that buffers output (from a
|
29
|
+
# child process) while ensuring that the output does not exhaust memory
|
30
|
+
# in the current process. it does this by preserving only the most
|
31
|
+
# interesting bits of data (start of lines, last in output).
|
32
|
+
class SafeOutputBuffer
|
33
|
+
|
34
|
+
# note utf-8 encodings for the Unicode elipsis character are inconsistent
|
35
|
+
# between ruby platforms (Windows vs Linux) and versions (1.8 vs 1.9).
|
36
|
+
ELLIPSIS = '...'
|
37
|
+
|
38
|
+
DEFAULT_MAX_LINE_COUNT = 64
|
39
|
+
DEFAULT_MAX_LINE_LENGTH = 256
|
40
|
+
|
41
|
+
attr_reader :buffer, :max_line_count, :max_line_length
|
42
|
+
|
43
|
+
# === Parameters
|
44
|
+
# @param [Array] buffer for lines
|
45
|
+
# @param [Integer] max_line_count to limit number of lines in buffer
|
46
|
+
# @param [Integer] max_line_length to truncate charcters from start of line
|
47
|
+
def initialize(buffer = [],
|
48
|
+
max_line_count = DEFAULT_MAX_LINE_COUNT,
|
49
|
+
max_line_length = DEFAULT_MAX_LINE_LENGTH)
|
50
|
+
raise ArgumentError.new('buffer is required') unless @buffer = buffer
|
51
|
+
raise ArgumentError.new('max_line_count is invalid') unless (@max_line_count = max_line_count) > 1
|
52
|
+
raise ArgumentError.new('max_line_length is invalid') unless (@max_line_length = max_line_length) > ELLIPSIS.length
|
53
|
+
end
|
54
|
+
|
55
|
+
def display_text; @buffer.join("\n"); end
|
56
|
+
|
57
|
+
# Buffers data with specified truncation.
|
58
|
+
#
|
59
|
+
# === Parameters
|
60
|
+
# @param [Object] data of any kind
|
61
|
+
def safe_buffer_data(data)
|
62
|
+
# note that the chomping ensures that the exact output cannot be
|
63
|
+
# preserved but the truncation would tend to eliminate trailing newlines
|
64
|
+
# in any case. if you want exact output then don't use safe buffering.
|
65
|
+
data = data.to_s.chomp
|
66
|
+
if @buffer.size >= @max_line_count
|
67
|
+
@buffer.shift
|
68
|
+
@buffer[0] = ELLIPSIS
|
69
|
+
end
|
70
|
+
if data.length > @max_line_length
|
71
|
+
truncation = [data.length - (@max_line_length - ELLIPSIS.length), 0].max
|
72
|
+
data = "#{data[0..(@max_line_length - ELLIPSIS.length - 1)]}#{ELLIPSIS}"
|
73
|
+
end
|
74
|
+
@buffer << data
|
75
|
+
true
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module RightPopen
|
26
|
+
|
27
|
+
# proxies calls to target to simplify popen3 implementation code and to make
|
28
|
+
# the proxied callbacks slightly more efficient.
|
29
|
+
class TargetProxy
|
30
|
+
|
31
|
+
HANDLER_NAME_TO_PARAMETER_COUNT = {
|
32
|
+
:exit_handler => 1,
|
33
|
+
:pid_handler => 1,
|
34
|
+
:size_limit_handler => 0,
|
35
|
+
:stderr_handler => 1,
|
36
|
+
:stdout_handler => 1,
|
37
|
+
:timeout_handler => 0,
|
38
|
+
:watch_handler => 1,
|
39
|
+
}
|
40
|
+
|
41
|
+
def initialize(options = {})
|
42
|
+
if options[:target].nil? &&
|
43
|
+
!(options.keys & HANDLER_NAME_TO_PARAMETER_COUNT.keys).empty?
|
44
|
+
raise ArgumentError, "Missing target"
|
45
|
+
end
|
46
|
+
@target = options[:target] # hold target reference (if any) against GC
|
47
|
+
|
48
|
+
# define an instance method for each handler that either proxies
|
49
|
+
# directly to the target method (with parameters) or else does nothing.
|
50
|
+
HANDLER_NAME_TO_PARAMETER_COUNT.each do |handler_name, parameter_count|
|
51
|
+
parameter_list = (1..parameter_count).map { |i| "p#{i}" }.join(', ')
|
52
|
+
instance_eval <<EOF
|
53
|
+
if @target && options[#{handler_name.inspect}]
|
54
|
+
@#{handler_name.to_s}_method = @target.method(options[#{handler_name.inspect}])
|
55
|
+
def #{handler_name.to_s}(#{parameter_list})
|
56
|
+
@#{handler_name.to_s}_method.call(#{parameter_list})
|
57
|
+
end
|
58
|
+
else
|
59
|
+
def #{handler_name.to_s}(#{parameter_list}); true; end
|
60
|
+
end
|
61
|
+
EOF
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end # RightPopen
|
67
|
+
end # RightScale
|
data/lib/right_popen/version.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#-- -*- mode: ruby; encoding: utf-8 -*-
|
2
|
-
# Copyright: Copyright (c) 2011 RightScale, Inc.
|
2
|
+
# Copyright: Copyright (c) 2011-2013 RightScale, Inc.
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -23,6 +23,6 @@
|
|
23
23
|
|
24
24
|
module RightScale
|
25
25
|
module RightPopen
|
26
|
-
VERSION = "1.
|
26
|
+
VERSION = "1.1.3"
|
27
27
|
end
|
28
28
|
end
|
data/right_popen.gemspec
CHANGED
@@ -47,8 +47,8 @@ EOF
|
|
47
47
|
end
|
48
48
|
spec.files = candidates.sort!
|
49
49
|
|
50
|
-
# Current implementation supports >= 0.
|
51
|
-
spec.
|
50
|
+
# Current implementation supports >= 1.0.0
|
51
|
+
spec.add_development_dependency(%q<eventmachine>, [">= 1.0.0"])
|
52
52
|
if is_windows
|
53
53
|
spec.add_runtime_dependency(%q<win32-process>, [">= 0.6.1"])
|
54
54
|
end
|
@@ -33,8 +33,6 @@ module RightScale::RightPopen
|
|
33
33
|
describe "#tick" do
|
34
34
|
context 'with a live child' do
|
35
35
|
before(:each) do
|
36
|
-
@process.should_receive(:status).and_return(nil)
|
37
|
-
@process.should_receive(:status=)
|
38
36
|
@input = flexmock("input")
|
39
37
|
@output = flexmock("output")
|
40
38
|
@read = flexmock("read")
|
@@ -155,14 +153,14 @@ module RightScale::RightPopen
|
|
155
153
|
a = Accumulator.new(@process, [], [], [], [])
|
156
154
|
status = flexmock("status")
|
157
155
|
flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(status)
|
158
|
-
@process.should_receive(:status).twice.and_return(nil).and_return(status)
|
159
|
-
@process.should_receive(:status=).with(status).once
|
160
156
|
a.tick.should be_true
|
161
157
|
end
|
162
158
|
|
163
159
|
it 'should return true if the process has already been waited on' do
|
164
|
-
@process.should_receive(:status).and_return(true)
|
165
160
|
a = Accumulator.new(@process, [], [], [], [])
|
161
|
+
status = flexmock("status")
|
162
|
+
flexmock(::Process).should_receive(:waitpid2).with(42, ::Process::WNOHANG).once.and_return(status)
|
163
|
+
a.tick.should be_true
|
166
164
|
a.tick.should be_true
|
167
165
|
end
|
168
166
|
end
|
@@ -188,8 +186,6 @@ module RightScale::RightPopen
|
|
188
186
|
it 'should transition from 0 to 1 as pipes are removed' do
|
189
187
|
pipe = flexmock("pipe")
|
190
188
|
read = flexmock("read")
|
191
|
-
@process.should_receive(:status).and_return(nil)
|
192
|
-
@process.should_receive(:status=)
|
193
189
|
a = Accumulator.new(@process, [pipe], [read], [], [])
|
194
190
|
flexmock(::IO).should_receive(:select).with([pipe], [], nil, 0.1).once.and_return([[pipe], [], []])
|
195
191
|
pipe.should_receive(:eof?).and_return(true)
|
@@ -203,7 +199,7 @@ module RightScale::RightPopen
|
|
203
199
|
|
204
200
|
describe "#cleanup" do
|
205
201
|
it 'should do nothing if no pipes are left open and the process is reaped' do
|
206
|
-
|
202
|
+
pending 'needs refactoring if actually in use'
|
207
203
|
a = Accumulator.new(@process, [], [], [], [])
|
208
204
|
flexmock(::Process).should_receive(:waitpid2).never
|
209
205
|
a.cleanup
|
@@ -212,16 +208,14 @@ module RightScale::RightPopen
|
|
212
208
|
it 'should just call waitpid if no pipes are left open' do
|
213
209
|
value = flexmock("value")
|
214
210
|
a = Accumulator.new(@process, [], [], [], [])
|
215
|
-
@process.should_receive(:status).and_return(nil)
|
216
211
|
flexmock(::Process).should_receive(:waitpid2).with(42).once.and_return(value)
|
217
|
-
@process.should_receive(:status=).with(value).once
|
218
212
|
a.cleanup
|
219
213
|
end
|
220
214
|
|
221
215
|
it 'should close all open pipes' do
|
216
|
+
pending 'needs refactoring if actually in use'
|
222
217
|
a, b, c = flexmock("a"), flexmock("b"), flexmock("c")
|
223
218
|
acc = Accumulator.new(@process, [a, b], [], [c], [])
|
224
|
-
@process.should_receive(:status).and_return(true)
|
225
219
|
[a, b].each {|fdes| fdes.should_receive(:close).with().once }
|
226
220
|
[a, b].each {|fdes| fdes.should_receive(:closed?).and_return(false) }
|
227
221
|
[c].each {|fdes| fdes.should_receive(:closed?).and_return(true) }
|
@@ -233,12 +227,10 @@ module RightScale::RightPopen
|
|
233
227
|
value = flexmock("value")
|
234
228
|
a, b, c = flexmock("a"), flexmock("b"), flexmock("c")
|
235
229
|
acc = Accumulator.new(@process, [b, c], [], [a], [])
|
236
|
-
@process.should_receive(:status).and_return(nil)
|
237
230
|
[a, b].each {|fdes| fdes.should_receive(:close).with().once }
|
238
231
|
[a, b].each {|fdes| fdes.should_receive(:closed?).and_return(false) }
|
239
232
|
[c].each {|fdes| fdes.should_receive(:closed?).and_return(true) }
|
240
233
|
flexmock(::Process).should_receive(:waitpid2).with(42).once.and_return(value)
|
241
|
-
@process.should_receive(:status=).with(value).once
|
242
234
|
acc.cleanup
|
243
235
|
end
|
244
236
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', 'spec_helper'))
|
2
|
+
require ::File.expand_path(::File.join(::File.dirname(__FILE__), '..', '..', 'lib', 'right_popen', 'safe_output_buffer'))
|
3
|
+
|
4
|
+
describe RightScale::RightPopen::SafeOutputBuffer do
|
5
|
+
|
6
|
+
context 'given a default buffer' do
|
7
|
+
subject { described_class.new }
|
8
|
+
|
9
|
+
it 'should limit line count and length' do
|
10
|
+
(described_class::DEFAULT_MAX_LINE_COUNT * 2).times do |line_index|
|
11
|
+
data = 'x' * rand(described_class::DEFAULT_MAX_LINE_LENGTH * 3)
|
12
|
+
subject.safe_buffer_data(data).should be_true
|
13
|
+
end
|
14
|
+
subject.buffer.size.should == described_class::DEFAULT_MAX_LINE_COUNT
|
15
|
+
subject.buffer.first.should == described_class::ELLIPSIS
|
16
|
+
subject.buffer.last.should_not == described_class::ELLIPSIS
|
17
|
+
subject.buffer.each do |line|
|
18
|
+
(line.length <= described_class::DEFAULT_MAX_LINE_LENGTH).should be_true
|
19
|
+
end
|
20
|
+
text = subject.display_text
|
21
|
+
text.should_not be_empty
|
22
|
+
text.lines.count.should == subject.buffer.size
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/spec/right_popen_spec.rb
CHANGED
@@ -1,286 +1,331 @@
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
2
|
require File.expand_path(File.join(File.dirname(__FILE__), 'runner'))
|
3
|
+
|
3
4
|
require 'stringio'
|
5
|
+
require 'tmpdir'
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
7
|
+
describe 'RightScale::RightPopen' do
|
8
|
+
def is_windows?
|
9
|
+
return !!(RUBY_PLATFORM =~ /mswin|mingw/)
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
|
13
|
-
runner = Runner.new
|
14
|
-
status = runner.run_right_popen(command)
|
15
|
-
status.status.exitstatus.should == 0
|
16
|
-
status.output_text.should == STANDARD_MESSAGE + "\n"
|
17
|
-
status.error_text.should == ERROR_MESSAGE + "\n"
|
18
|
-
status.pid.should > 0
|
19
|
-
end
|
12
|
+
let(:runner) { ::RightScale::RightPopen::Runner.new }
|
20
13
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
14
|
+
it "should correctly handle many small processes [async]" do
|
15
|
+
pending 'Set environment variable TEST_STRESS to enable' unless ENV['TEST_STRESS']
|
16
|
+
run_count = 100
|
17
|
+
command = is_windows? ? ['cmd.exe', '/c', 'exit 0'] : ['sh', '-c', 'exit 0']
|
18
|
+
@completed = 0
|
19
|
+
@started = 0
|
20
|
+
run_cmd = Proc.new do
|
21
|
+
runner.do_right_popen3_async(command, runner_options={}, popen3_options={}) do |runner_status|
|
22
|
+
@completed += 1
|
23
|
+
runner_status.status.exitstatus.should == 0
|
24
|
+
runner_status.output_text.should == ''
|
25
|
+
runner_status.error_text.should == ''
|
26
|
+
runner_status.pid.should > 0
|
27
|
+
end
|
28
|
+
@started += 1
|
29
|
+
if @started < run_count
|
30
|
+
EM.next_tick { run_cmd.call }
|
31
|
+
end
|
29
32
|
end
|
33
|
+
EM.run do
|
34
|
+
EM.next_tick { run_cmd.call }
|
30
35
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
command = is_windows? ? "cmd.exe /c exit 0" : "exit 0"
|
35
|
-
runner = Runner.new
|
36
|
-
@completed = 0
|
37
|
-
@started = 0
|
38
|
-
run_cmd = Proc.new do
|
39
|
-
runner.do_right_popen(command) do |status|
|
40
|
-
@completed += 1
|
41
|
-
status.status.exitstatus.should == 0
|
42
|
-
status.output_text.should == ''
|
43
|
-
status.error_text.should == ''
|
44
|
-
status.pid.should > 0
|
45
|
-
end
|
46
|
-
@started += 1
|
47
|
-
if @started < TO_RUN
|
48
|
-
EM.next_tick { run_cmd.call }
|
36
|
+
EM::PeriodicTimer.new(1) do
|
37
|
+
if @completed >= run_count
|
38
|
+
EM.stop
|
49
39
|
end
|
50
40
|
end
|
51
|
-
|
52
|
-
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
[:sync, :async].each do |synchronicity|
|
45
|
+
|
46
|
+
context synchronicity do
|
47
|
+
|
48
|
+
it "should redirect output" do
|
49
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
|
50
|
+
runner_status = runner.run_right_popen3(synchronicity, command)
|
51
|
+
runner_status.should_not be_nil
|
52
|
+
runner_status.status.should_not be_nil
|
53
|
+
runner_status.status.exitstatus.should == 0
|
54
|
+
runner_status.output_text.should == STANDARD_MESSAGE + "\n"
|
55
|
+
runner_status.error_text.should == ERROR_MESSAGE + "\n"
|
56
|
+
runner_status.pid.should > 0
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should return the right status" do
|
60
|
+
ruby = `which ruby`.chomp # which is assumed to be on the PATH for the Windows case
|
61
|
+
command = [
|
62
|
+
ruby,
|
63
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'produce_status.rb')),
|
64
|
+
EXIT_STATUS
|
65
|
+
]
|
66
|
+
status = runner.run_right_popen3(synchronicity, command)
|
67
|
+
status.status.exitstatus.should == EXIT_STATUS
|
68
|
+
status.output_text.should == ''
|
69
|
+
status.error_text.should == ''
|
70
|
+
status.pid.should > 0
|
71
|
+
end
|
53
72
|
|
54
|
-
|
55
|
-
|
56
|
-
|
73
|
+
it "should close all IO handlers, except STDIN, STDOUT and STDERR" do
|
74
|
+
GC.start
|
75
|
+
command = [
|
76
|
+
RUBY_CMD,
|
77
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'produce_status.rb')),
|
78
|
+
EXIT_STATUS
|
79
|
+
]
|
80
|
+
status = runner.run_right_popen3(synchronicity, command)
|
81
|
+
status.status.exitstatus.should == EXIT_STATUS
|
82
|
+
useless_handlers = 0
|
83
|
+
ObjectSpace.each_object(IO) do |io|
|
84
|
+
if ![STDIN, STDOUT, STDERR].include?(io)
|
85
|
+
useless_handlers += 1 unless io.closed?
|
57
86
|
end
|
58
87
|
end
|
88
|
+
useless_handlers.should == 0
|
59
89
|
end
|
60
|
-
end
|
61
90
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
useless_handlers += 1 unless io.closed?
|
91
|
+
it "should preserve the integrity of stdout when stderr is unavailable" do
|
92
|
+
count = LARGE_OUTPUT_COUNTER
|
93
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_stdout_only.rb'))}\" #{count}"
|
94
|
+
status = runner.run_right_popen3(synchronicity, command)
|
95
|
+
status.status.exitstatus.should == 0
|
96
|
+
|
97
|
+
results = ''
|
98
|
+
count.times do |i|
|
99
|
+
results << "stdout #{i}\n"
|
72
100
|
end
|
101
|
+
status.output_text.should == results
|
102
|
+
status.error_text.should == ''
|
103
|
+
status.pid.should > 0
|
73
104
|
end
|
74
|
-
useless_handlers.should == 0
|
75
|
-
end
|
76
105
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
status.status.exitstatus.should == 0
|
106
|
+
it "should preserve the integrity of stderr when stdout is unavailable" do
|
107
|
+
count = LARGE_OUTPUT_COUNTER
|
108
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_stderr_only.rb'))}\" #{count}"
|
109
|
+
status = runner.run_right_popen3(synchronicity, command)
|
110
|
+
status.status.exitstatus.should == 0
|
83
111
|
|
84
|
-
|
85
|
-
|
86
|
-
|
112
|
+
results = ''
|
113
|
+
count.times do |i|
|
114
|
+
results << "stderr #{i}\n"
|
115
|
+
end
|
116
|
+
status.error_text.should == results
|
117
|
+
status.output_text.should == ''
|
118
|
+
status.pid.should > 0
|
87
119
|
end
|
88
|
-
status.output_text.should == results
|
89
|
-
status.error_text.should == ''
|
90
|
-
status.pid.should > 0
|
91
|
-
end
|
92
120
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
121
|
+
it "should preserve interleaved output when yielding CPU on consumer thread" do
|
122
|
+
lines = 11
|
123
|
+
exit_code = 42
|
124
|
+
repeats = 5
|
125
|
+
force_yield = 0.1
|
126
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{lines} #{exit_code}"
|
127
|
+
actual_output = StringIO.new
|
128
|
+
actual_error = StringIO.new
|
129
|
+
puts
|
130
|
+
stats = runner.run_right_popen3(synchronicity, command, :repeats=>repeats, :force_yield=>force_yield) do |status|
|
131
|
+
status.status.exitstatus.should == exit_code
|
132
|
+
status.pid.should > 0
|
133
|
+
actual_output << status.output_text
|
134
|
+
actual_error << status.error_text
|
135
|
+
end
|
136
|
+
puts
|
137
|
+
stats.size.should == repeats
|
138
|
+
|
139
|
+
expected_output = StringIO.new
|
140
|
+
repeats.times do
|
141
|
+
lines.times do |i|
|
142
|
+
expected_output << "stdout #{i}\n"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
actual_output.string.should == expected_output.string
|
99
146
|
|
100
|
-
|
101
|
-
|
102
|
-
|
147
|
+
expected_error = StringIO.new
|
148
|
+
repeats.times do
|
149
|
+
lines.times do |i|
|
150
|
+
(expected_error << "stderr #{i}\n") if 0 == i % 10
|
151
|
+
end
|
152
|
+
end
|
153
|
+
actual_error.string.should == expected_error.string
|
103
154
|
end
|
104
|
-
status.error_text.should == results
|
105
|
-
status.output_text.should == ''
|
106
|
-
status.pid.should > 0
|
107
|
-
end
|
108
155
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{lines} #{exit_code}"
|
115
|
-
runner = Runner.new
|
116
|
-
actual_output = StringIO.new
|
117
|
-
actual_error = StringIO.new
|
118
|
-
puts
|
119
|
-
stats = runner.run_right_popen(command, :repeats=>repeats, :force_yield=>force_yield) do |status|
|
156
|
+
it "should preserve interleaved output when process is spewing rapidly" do
|
157
|
+
lines = LARGE_OUTPUT_COUNTER
|
158
|
+
exit_code = 99
|
159
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{lines} #{exit_code}"
|
160
|
+
status = runner.run_right_popen3(synchronicity, command, :timeout=>10)
|
120
161
|
status.status.exitstatus.should == exit_code
|
121
|
-
status.pid.should > 0
|
122
|
-
actual_output << status.output_text
|
123
|
-
actual_error << status.error_text
|
124
|
-
print '+'
|
125
|
-
end
|
126
|
-
puts
|
127
|
-
stats.size.should == repeats
|
128
162
|
|
129
|
-
|
130
|
-
repeats.times do
|
163
|
+
expected_output = StringIO.new
|
131
164
|
lines.times do |i|
|
132
165
|
expected_output << "stdout #{i}\n"
|
133
166
|
end
|
134
|
-
|
135
|
-
actual_output.string.should == expected_output.string
|
167
|
+
status.output_text.should == expected_output.string
|
136
168
|
|
137
|
-
|
138
|
-
repeats.times do
|
169
|
+
expected_error = StringIO.new
|
139
170
|
lines.times do |i|
|
140
171
|
(expected_error << "stderr #{i}\n") if 0 == i % 10
|
141
172
|
end
|
173
|
+
status.error_text.should == expected_error.string
|
174
|
+
status.pid.should > 0
|
142
175
|
end
|
143
|
-
actual_error.string.should == expected_error.string
|
144
|
-
end
|
145
176
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
lines.times do |i|
|
156
|
-
expected_output << "stdout #{i}\n"
|
177
|
+
it "should setup environment variables" do
|
178
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
|
179
|
+
status = runner.run_right_popen3(synchronicity, command)
|
180
|
+
status.status.exitstatus.should == 0
|
181
|
+
status.output_text.should_not include('_test_')
|
182
|
+
status = runner.run_right_popen3(synchronicity, command, :env=>{ :__test__ => '42' })
|
183
|
+
status.status.exitstatus.should == 0
|
184
|
+
status.output_text.should match(/^__test__=42$/)
|
185
|
+
status.pid.should > 0
|
157
186
|
end
|
158
|
-
status.output_text.should == expected_output.string
|
159
187
|
|
160
|
-
|
161
|
-
|
162
|
-
|
188
|
+
it "should restore environment variables" do
|
189
|
+
begin
|
190
|
+
ENV['__test__'] = '41'
|
191
|
+
old_envs = {}
|
192
|
+
ENV.each { |k, v| old_envs[k] = v }
|
193
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
|
194
|
+
status = runner.run_right_popen3(synchronicity, command, :env=>{ :__test__ => '42' })
|
195
|
+
status.status.exitstatus.should == 0
|
196
|
+
status.output_text.should match(/^__test__=42$/)
|
197
|
+
ENV.each { |k, v| old_envs[k].should == v }
|
198
|
+
old_envs.each { |k, v| ENV[k].should == v }
|
199
|
+
status.pid.should > 0
|
200
|
+
ensure
|
201
|
+
ENV.delete('__test__')
|
202
|
+
end
|
163
203
|
end
|
164
|
-
status.error_text.should == expected_error.string
|
165
|
-
status.pid.should > 0
|
166
|
-
end
|
167
204
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
205
|
+
if is_windows?
|
206
|
+
# FIX: this behavior is currently specific to Windows but should probably be
|
207
|
+
# implemented for Linux.
|
208
|
+
it "should merge the PATH variable instead of overriding it" do
|
209
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
|
210
|
+
status = runner.run_right_popen3(synchronicity, command, :env=>{ 'PATH' => "c:/bogus\\bin" })
|
211
|
+
status.status.exitstatus.should == 0
|
212
|
+
status.output_text.should include('c:\\bogus\\bin;')
|
213
|
+
status.pid.should > 0
|
214
|
+
end
|
215
|
+
else
|
216
|
+
it "should allow running bash command lines starting with a built-in command" do
|
217
|
+
command = "for i in 1 2 3 4 5; do echo $i;done"
|
218
|
+
status = runner.run_right_popen3(synchronicity, command)
|
219
|
+
status.status.exitstatus.should == 0
|
220
|
+
status.output_text.should == "1\n2\n3\n4\n5\n"
|
221
|
+
status.pid.should > 0
|
222
|
+
end
|
179
223
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
old_envs.each { |k, v| ENV[k].should == v }
|
192
|
-
status.pid.should > 0
|
193
|
-
ensure
|
194
|
-
ENV.delete('__test__')
|
224
|
+
it "should support running background processes" do
|
225
|
+
command = "(sleep 20)&"
|
226
|
+
now = Time.now
|
227
|
+
status = runner.run_right_popen3(synchronicity, command)
|
228
|
+
finished = Time.now
|
229
|
+
(finished - now).should < 20
|
230
|
+
status.did_timeout.should be_false
|
231
|
+
status.status.exitstatus.should == 0
|
232
|
+
status.output_text.should == ""
|
233
|
+
status.pid.should > 0
|
234
|
+
end
|
195
235
|
end
|
196
|
-
end
|
197
236
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
it 'should merge the PATH variable instead of overriding it' do
|
202
|
-
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
|
203
|
-
runner = Runner.new
|
204
|
-
status = runner.run_right_popen(command, :env=>{ 'PATH' => "c:/bogus\\bin" })
|
237
|
+
it "should support raw command arguments" do
|
238
|
+
command = is_windows? ? ["cmd.exe", "/c", "echo", "*"] : ["echo", "*"]
|
239
|
+
status = runner.run_right_popen3(synchronicity, command)
|
205
240
|
status.status.exitstatus.should == 0
|
206
|
-
status.output_text.should
|
241
|
+
status.output_text.should == "*\n"
|
207
242
|
status.pid.should > 0
|
208
243
|
end
|
209
|
-
|
210
|
-
it
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
244
|
+
|
245
|
+
it "should run repeatedly without leaking resources" do
|
246
|
+
pending 'Set environment variable TEST_LEAK to enable' unless ENV['TEST_LEAK']
|
247
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
|
248
|
+
stats = runner.run_right_popen3(synchronicity, command, :repeats=>REPEAT_TEST_COUNTER)
|
249
|
+
stats.each do |status|
|
250
|
+
status.status.exitstatus.should == 0
|
251
|
+
status.output_text.should == STANDARD_MESSAGE + "\n"
|
252
|
+
status.error_text.should == ERROR_MESSAGE + "\n"
|
253
|
+
status.pid.should > 0
|
254
|
+
end
|
217
255
|
end
|
218
256
|
|
219
|
-
it
|
220
|
-
command = "(
|
221
|
-
|
222
|
-
runner = Runner.new
|
223
|
-
status = runner.run_right_popen(command)
|
224
|
-
finished = Time.now
|
225
|
-
(finished - now).should < 20
|
226
|
-
status.did_timeout.should be_false
|
257
|
+
it "should pass input to child process" do
|
258
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'increment.rb'))}\""
|
259
|
+
status = runner.run_right_popen3(synchronicity, command, :input=>"42\n")
|
227
260
|
status.status.exitstatus.should == 0
|
228
|
-
status.output_text.should == ""
|
261
|
+
status.output_text.should == "43\n"
|
262
|
+
status.error_text.should be_empty
|
229
263
|
status.pid.should > 0
|
230
264
|
end
|
231
|
-
end
|
232
265
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
266
|
+
it "should run long child process without any watches by default" do
|
267
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'sleeper.rb'))}\""
|
268
|
+
runner_status = runner.run_right_popen3(synchronicity, command, :timeout=>nil)
|
269
|
+
runner_status.status.exitstatus.should == 0
|
270
|
+
runner_status.did_timeout.should be_false
|
271
|
+
runner_status.output_text.should == "To sleep... 0\nTo sleep... 1\nTo sleep... 2\nTo sleep... 3\nThe sleeper must awaken.\n"
|
272
|
+
runner_status.error_text.should == "Perchance to dream... 0\nPerchance to dream... 1\nPerchance to dream... 2\nPerchance to dream... 3\n"
|
273
|
+
end
|
241
274
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
status.output_text.should == STANDARD_MESSAGE + "\n"
|
250
|
-
status.error_text.should == ERROR_MESSAGE + "\n"
|
251
|
-
status.pid.should > 0
|
275
|
+
it "should interrupt watched child process when timeout expires" do
|
276
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'sleeper.rb'))}\" 10"
|
277
|
+
runner_status = runner.run_right_popen3(synchronicity, command, :expect_timeout=>true, :timeout=>0.1)
|
278
|
+
runner_status.status.success?.should be_false
|
279
|
+
runner_status.did_timeout.should be_true
|
280
|
+
runner_status.output_text.should_not be_empty
|
281
|
+
runner_status.error_text.should_not be_empty
|
252
282
|
end
|
253
|
-
end
|
254
283
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
end
|
284
|
+
it "should allow watched child to write files up to size limit" do
|
285
|
+
::Dir.mktmpdir do |watched_dir|
|
286
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'writer.rb'))}\" \"#{watched_dir}\""
|
287
|
+
runner_status = runner.run_right_popen3(synchronicity, command, :size_limit_bytes=>1000, :watch_directory=>watched_dir, :timeout=>10)
|
288
|
+
runner_status.status.success?.should be_true
|
289
|
+
runner_status.did_size_limit.should be_false
|
290
|
+
end
|
291
|
+
end
|
264
292
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
293
|
+
it "should interrupt watched child at size limit" do
|
294
|
+
::Dir.mktmpdir do |watched_dir|
|
295
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'writer.rb'))}\" \"#{watched_dir}\""
|
296
|
+
runner_status = runner.run_right_popen3(synchronicity, command, :expect_size_limit=>true, :size_limit_bytes=>100, :watch_directory=>watched_dir, :timeout=>10)
|
297
|
+
runner_status.status.success?.should be_false
|
298
|
+
runner_status.did_size_limit.should be_true
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
it "should handle child processes that close stdout but keep running" do
|
303
|
+
pending 'not implemented for windows' if is_windows? && :sync != synchronicity
|
304
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'stdout.rb'))}\""
|
305
|
+
runner_status = runner.run_right_popen3(synchronicity, command, :expect_timeout=>true, :timeout=>2)
|
306
|
+
runner_status.output_text.should be_empty
|
307
|
+
runner_status.error_text.should == "Closing stdout\n"
|
308
|
+
runner_status.did_timeout.should be_true
|
309
|
+
end
|
274
310
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
311
|
+
it "should handle child processes that spawn long running background processes" do
|
312
|
+
pending 'not implemented for windows' if is_windows?
|
313
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'background.rb'))}\""
|
314
|
+
status = runner.run_right_popen3(synchronicity, command)
|
315
|
+
status.status.exitstatus.should == 0
|
316
|
+
status.did_timeout.should be_false
|
317
|
+
status.output_text.should be_empty
|
318
|
+
status.error_text.should be_empty
|
319
|
+
end
|
320
|
+
|
321
|
+
it "should run long child process without any watches by default" do
|
322
|
+
command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'sleeper.rb'))}\""
|
323
|
+
runner_status = runner.run_right_popen3(synchronicity, command, :timeout=>nil)
|
324
|
+
runner_status.status.exitstatus.should == 0
|
325
|
+
runner_status.did_timeout.should be_false
|
326
|
+
runner_status.output_text.should == "To sleep... 0\nTo sleep... 1\nTo sleep... 2\nTo sleep... 3\nThe sleeper must awaken.\n"
|
327
|
+
runner_status.error_text.should == "Perchance to dream... 0\nPerchance to dream... 1\nPerchance to dream... 2\nPerchance to dream... 3\n"
|
328
|
+
end
|
284
329
|
end
|
285
330
|
end
|
286
331
|
end
|