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.
@@ -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
@@ -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.0.21"
26
+ VERSION = "1.1.3"
27
27
  end
28
28
  end
@@ -47,8 +47,8 @@ EOF
47
47
  end
48
48
  spec.files = candidates.sort!
49
49
 
50
- # Current implementation supports >= 0.12.11
51
- spec.add_runtime_dependency(%q<eventmachine>, [">= 0.12.11"])
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
@@ -1,6 +1,9 @@
1
1
  count = ARGV[0] ? ARGV[0].to_i : 1
2
2
  exit_code = ARGV[1] ? ARGV[1].to_i : 0
3
3
 
4
+ STDOUT.sync=true
5
+ STDERR.sync=true
6
+
4
7
  count.times do |i|
5
8
  $stderr.puts "stderr #{i}" if 0 == i % 10
6
9
  $stdout.puts "stdout #{i}"
@@ -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
- @process.should_receive(:status).and_return(true)
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
@@ -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
- module RightScale
6
- describe 'popen3' do
7
- def is_windows?
8
- return !!(RUBY_PLATFORM =~ /mswin/)
9
- end
7
+ describe 'RightScale::RightPopen' do
8
+ def is_windows?
9
+ return !!(RUBY_PLATFORM =~ /mswin|mingw/)
10
+ end
10
11
 
11
- it 'should redirect output' do
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
- it 'should return the right status' do
22
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_status.rb'))}\" #{EXIT_STATUS}"
23
- runner = Runner.new
24
- status = runner.run_right_popen(command)
25
- status.status.exitstatus.should == EXIT_STATUS
26
- status.output_text.should == ''
27
- status.error_text.should == ''
28
- status.pid.should > 0
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
- it 'should correctly handle many small processes' do
32
- pending 'Set environment variable TEST_STRESS to enable' unless ENV['TEST_STRESS']
33
- TO_RUN = 100
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
- EM.run do
52
- EM.next_tick { run_cmd.call }
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
- EM::PeriodicTimer.new(1) do
55
- if @completed >= TO_RUN
56
- EM.stop
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
- it 'should close all IO handlers, except STDIN, STDOUT and STDERR' do
63
- GC.start
64
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_status.rb'))}\" #{EXIT_STATUS}"
65
- runner = Runner.new
66
- status = runner.run_right_popen(command)
67
- status.status.exitstatus.should == EXIT_STATUS
68
- useless_handlers = 0
69
- ObjectSpace.each_object(IO) do |io|
70
- if ![STDIN, STDOUT, STDERR].include?(io)
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
- it 'should preserve the integrity of stdout when stderr is unavailable' do
78
- count = LARGE_OUTPUT_COUNTER
79
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_stdout_only.rb'))}\" #{count}"
80
- runner = Runner.new
81
- status = runner.run_right_popen(command)
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
- results = ''
85
- count.times do |i|
86
- results << "stdout #{i}\n"
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
- it 'should preserve the integrity of stderr when stdout is unavailable' do
94
- count = LARGE_OUTPUT_COUNTER
95
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_stderr_only.rb'))}\" #{count}"
96
- runner = Runner.new
97
- status = runner.run_right_popen(command)
98
- status.status.exitstatus.should == 0
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
- results = ''
101
- count.times do |i|
102
- results << "stderr #{i}\n"
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
- it 'should preserve interleaved output when yielding CPU on consumer thread' do
110
- lines = 11
111
- exit_code = 42
112
- repeats = 5
113
- force_yield = 0.1
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
- expected_output = StringIO.new
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
- end
135
- actual_output.string.should == expected_output.string
167
+ status.output_text.should == expected_output.string
136
168
 
137
- expected_error = StringIO.new
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
- it 'should preserve interleaved output when process is spewing rapidly' do
147
- lines = LARGE_OUTPUT_COUNTER
148
- exit_code = 99
149
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_mixed_output.rb'))}\" #{lines} #{exit_code}"
150
- runner = Runner.new
151
- status = runner.run_right_popen(command)
152
- status.status.exitstatus.should == exit_code
153
-
154
- expected_output = StringIO.new
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
- expected_error = StringIO.new
161
- lines.times do |i|
162
- (expected_error << "stderr #{i}\n") if 0 == i % 10
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
- it 'should setup environment variables' do
169
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
170
- runner = Runner.new
171
- status = runner.run_right_popen(command)
172
- status.status.exitstatus.should == 0
173
- status.output_text.should_not include('_test_')
174
- status = runner.run_right_popen(command, :env=>{ :__test__ => '42' })
175
- status.status.exitstatus.should == 0
176
- status.output_text.should match(/^__test__=42$/)
177
- status.pid.should > 0
178
- end
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
- it 'should restore environment variables' do
181
- begin
182
- ENV['__test__'] = '41'
183
- old_envs = {}
184
- ENV.each { |k, v| old_envs[k] = v }
185
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'print_env.rb'))}\""
186
- runner = Runner.new
187
- status = runner.run_right_popen(command, :env=>{ :__test__ => '42' })
188
- status.status.exitstatus.should == 0
189
- status.output_text.should match(/^__test__=42$/)
190
- ENV.each { |k, v| old_envs[k].should == v }
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
- if is_windows?
199
- # FIX: this behavior is currently specific to Windows but should probably be
200
- # implemented for Linux.
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 include('c:\\bogus\\bin;')
241
+ status.output_text.should == "*\n"
207
242
  status.pid.should > 0
208
243
  end
209
- else
210
- it 'should allow running bash command lines starting with a built-in command' do
211
- command = "for i in 1 2 3 4 5; do echo $i;done"
212
- runner = Runner.new
213
- status = runner.run_right_popen(command)
214
- status.status.exitstatus.should == 0
215
- status.output_text.should == "1\n2\n3\n4\n5\n"
216
- status.pid.should > 0
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 'should support running background processes' do
220
- command = "(sleep 20)&"
221
- now = Time.now
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
- it 'should support raw command arguments' do
234
- command = is_windows? ? ["cmd.exe", "/c", "echo", "*"] : ["echo", "*"]
235
- runner = Runner.new
236
- status = runner.run_right_popen(command)
237
- status.status.exitstatus.should == 0
238
- status.output_text.should == "*\n"
239
- status.pid.should > 0
240
- end
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
- it 'should run repeatedly without leaking resources' do
243
- pending 'Set environment variable TEST_LEAK to enable' unless ENV['TEST_LEAK']
244
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'produce_output.rb'))}\" \"#{STANDARD_MESSAGE}\" \"#{ERROR_MESSAGE}\""
245
- runner = Runner.new
246
- stats = runner.run_right_popen(command, :repeats=>REPEAT_TEST_COUNTER)
247
- stats.each do |status|
248
- status.status.exitstatus.should == 0
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
- it 'should pass input to child process' do
256
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'increment.rb'))}\""
257
- runner = Runner.new
258
- status = runner.run_right_popen(command, :input=>"42\n")
259
- status.status.exitstatus.should == 0
260
- status.output_text.should == "43\n"
261
- status.error_text.should be_empty
262
- status.pid.should > 0
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
- it 'should handle child processes that close stdout but keep running' do
266
- pending 'not implemented for windows' if is_windows?
267
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'stdout.rb'))}\""
268
- runner = Runner.new
269
- status = runner.run_right_popen(command, :expect_timeout=>true, :timeout=>2)
270
- status.did_timeout.should be_true
271
- status.output_text.should be_empty
272
- status.error_text.should == "Closing stdout\n"
273
- end
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
- it 'should handle child processes that spawn long running background processes' do
276
- pending 'not implemented for windows' if is_windows?
277
- command = "\"#{RUBY_CMD}\" \"#{File.expand_path(File.join(File.dirname(__FILE__), 'background.rb'))}\""
278
- runner = Runner.new
279
- status = runner.run_right_popen(command)
280
- status.status.exitstatus.should == 0
281
- status.did_timeout.should be_false
282
- status.output_text.should be_empty
283
- status.error_text.should be_empty
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