right_popen 1.0.21 → 1.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/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
|