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.
@@ -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