posix-spawn 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,212 @@
1
+ require 'posix/spawn'
2
+
3
+ module POSIX
4
+ module Spawn
5
+ # POSIX::Spawn::Child includes logic for executing child processes and
6
+ # reading/writing from their standard input, output, and error streams. It's
7
+ # designed to take all input in a single string and provides all output
8
+ # (stderr and stdout) as single strings and is therefore not well-suited
9
+ # to streaming large quantities of data in and out of commands.
10
+ #
11
+ # Create and run a process to completion:
12
+ #
13
+ # >> child = POSIX::Spawn::Child.new('git', '--help')
14
+ #
15
+ # Retrieve stdout or stderr output:
16
+ #
17
+ # >> child.out
18
+ # => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
19
+ # >> child.err
20
+ # => ""
21
+ #
22
+ # Check process exit status information:
23
+ #
24
+ # >> child.status
25
+ # => #<Process::Status: pid=80718,exited(0)>
26
+ #
27
+ # To write data on the new process's stdin immediately after spawning:
28
+ #
29
+ # >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2')
30
+ # >> child.out
31
+ # "42\n"
32
+ #
33
+ # Q: Why use POSIX::Spawn::Child instead of popen3, hand rolled fork/exec
34
+ # code, or Process::spawn?
35
+ #
36
+ # - It's more efficient than popen3 and provides meaningful process
37
+ # hierarchies because it performs a single fork/exec. (popen3 double forks
38
+ # to avoid needing to collect the exit status and also calls
39
+ # Process::detach which creates a Ruby Thread!!!!).
40
+ #
41
+ # - It handles all max pipe buffer (PIPE_BUF) hang cases when reading and
42
+ # writing semi-large amounts of data. This is non-trivial to implement
43
+ # correctly and must be accounted for with popen3, spawn, or hand rolled
44
+ # fork/exec code.
45
+ #
46
+ # - It's more portable than hand rolled pipe, fork, exec code because
47
+ # fork(2) and exec aren't available on all platforms. In those cases,
48
+ # POSIX::Spawn::Child falls back to using whatever janky substitutes
49
+ # the platform provides.
50
+ class Child
51
+ include POSIX::Spawn
52
+
53
+ # Spawn a new process, write all input and read all output, and wait for
54
+ # the program to exit. Supports the standard spawn interface as described
55
+ # in the POSIX::Spawn module documentation:
56
+ #
57
+ # new([env], command, [argv1, ...], [options])
58
+ #
59
+ # The following options are supported in addition to the standard
60
+ # POSIX::Spawn options:
61
+ #
62
+ # :input => str Write str to the new process's standard input.
63
+ # :timeout => int Maximum number of seconds to allow the process
64
+ # to execute before aborting with a TimeoutExceeded
65
+ # exception.
66
+ # :max => total Maximum number of bytes of output to allow the
67
+ # process to generate before aborting with a
68
+ # MaximumOutputExceeded exception.
69
+ #
70
+ # Returns a new Child instance whose underlying process has already
71
+ # executed to completion. The out, err, and status attributes are
72
+ # immediately available.
73
+ def initialize(*args)
74
+ @env, @argv, options = extract_process_spawn_arguments(*args)
75
+ @options = options.dup
76
+ @input = @options.delete(:input)
77
+ @timeout = @options.delete(:timeout)
78
+ @max = @options.delete(:max)
79
+ @options.delete(:chdir) if @options[:chdir].nil?
80
+ exec!
81
+ end
82
+
83
+ # All data written to the child process's stdout stream as a String.
84
+ attr_reader :out
85
+
86
+ # All data written to the child process's stderr stream as a String.
87
+ attr_reader :err
88
+
89
+ # A Process::Status object with information on how the child exited.
90
+ attr_reader :status
91
+
92
+ # Total command execution time (wall-clock time)
93
+ attr_reader :runtime
94
+
95
+ # Determine if the process did exit with a zero exit status.
96
+ def success?
97
+ @status && @status.success?
98
+ end
99
+
100
+ private
101
+ # Execute command, write input, and read output. This is called
102
+ # immediately when a new instance of this object is initialized.
103
+ def exec!
104
+ # spawn the process and hook up the pipes
105
+ pid, stdin, stdout, stderr = popen4(@env, *(@argv + [@options]))
106
+
107
+ # async read from all streams into buffers
108
+ @out, @err = read_and_write(@input, stdin, stdout, stderr, @timeout, @max)
109
+
110
+ # grab exit status
111
+ @status = waitpid(pid)
112
+ rescue Object => boom
113
+ [stdin, stdout, stderr].each { |fd| fd.close rescue nil }
114
+ if @status.nil?
115
+ ::Process.kill('TERM', pid) rescue nil
116
+ @status = waitpid(pid) rescue nil
117
+ end
118
+ raise
119
+ ensure
120
+ # let's be absolutely certain these are closed
121
+ [stdin, stdout, stderr].each { |fd| fd.close rescue nil }
122
+ end
123
+
124
+ # Maximum buffer size for reading
125
+ BUFSIZE = (32 * 1024)
126
+
127
+ # Start a select loop writing any input on the child's stdin and reading
128
+ # any output from the child's stdout or stderr.
129
+ #
130
+ # input - String input to write on stdin. May be nil.
131
+ # stdin - The write side IO object for the child's stdin stream.
132
+ # stdout - The read side IO object for the child's stdout stream.
133
+ # stderr - The read side IO object for the child's stderr stream.
134
+ # timeout - An optional Numeric specifying the total number of seconds
135
+ # the read/write operations should occur for.
136
+ #
137
+ # Returns an [out, err] tuple where both elements are strings with all
138
+ # data written to the stdout and stderr streams, respectively.
139
+ # Raises TimeoutExceeded when all data has not been read / written within
140
+ # the duration specified in the timeout argument.
141
+ # Raises MaximumOutputExceeded when the total number of bytes output
142
+ # exceeds the amount specified by the max argument.
143
+ def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil)
144
+ input ||= ''
145
+ max = nil if max && max <= 0
146
+ out, err = '', ''
147
+ offset = 0
148
+
149
+ timeout = nil if timeout && timeout <= 0.0
150
+ @runtime = 0.0
151
+ start = Time.now
152
+
153
+ writers = [stdin]
154
+ readers = [stdout, stderr]
155
+ t = timeout
156
+ while readers.any? || writers.any?
157
+ ready = IO.select(readers, writers, readers + writers, t)
158
+ raise TimeoutExceeded if ready.nil?
159
+
160
+ # write to stdin stream
161
+ ready[1].each do |fd|
162
+ begin
163
+ boom = nil
164
+ size = fd.write_nonblock(input)
165
+ input = input[size, input.size]
166
+ rescue Errno::EPIPE => boom
167
+ rescue Errno::EAGAIN, Errno::EINTR
168
+ end
169
+ if boom || input.size == 0
170
+ stdin.close
171
+ writers.delete(stdin)
172
+ end
173
+ end
174
+
175
+ # read from stdout and stderr streams
176
+ ready[0].each do |fd|
177
+ buf = (fd == stdout) ? out : err
178
+ begin
179
+ buf << fd.readpartial(BUFSIZE)
180
+ rescue Errno::EAGAIN, Errno::EINTR
181
+ rescue EOFError
182
+ readers.delete(fd)
183
+ fd.close
184
+ end
185
+ end
186
+
187
+ # keep tabs on the total amount of time we've spent here
188
+ @runtime = Time.now - start
189
+ if timeout
190
+ t = timeout - @runtime
191
+ raise TimeoutExceeded if t < 0.0
192
+ end
193
+
194
+ # maybe we've hit our max output
195
+ if max && ready[0].any? && (out.size + err.size) > max
196
+ raise MaximumOutputExceeded
197
+ end
198
+ end
199
+
200
+ [out, err]
201
+ end
202
+
203
+ # Wait for the child process to exit
204
+ #
205
+ # Returns the Process::Status object obtained by reaping the process.
206
+ def waitpid(pid)
207
+ ::Process::waitpid(pid)
208
+ $?
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,5 @@
1
+ module POSIX
2
+ module Spawn
3
+ VERSION = '0.3.0'
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../lib/posix/spawn/version', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'posix-spawn'
5
+ s.version = POSIX::Spawn::VERSION
6
+
7
+ s.summary = 'posix_spawnp(2) for ruby'
8
+ s.description = 'posix-spawn uses posix_spawnp(2) for faster process spawning'
9
+
10
+ s.homepage = 'http://github.com/rtomayko/posix-spawn'
11
+ s.has_rdoc = false
12
+
13
+ s.authors = ['Ryan Tomayko', 'Aman Gupta']
14
+ s.email = ['r@tomayko.com', 'aman@tmm1.net']
15
+
16
+ s.add_development_dependency 'rake-compiler', '0.7.6'
17
+
18
+ s.extensions = ['ext/extconf.rb']
19
+ s.executables << 'posix-spawn-benchmark'
20
+ s.require_paths = ['lib']
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.extra_rdoc_files = %w[ COPYING HACKING ]
24
+ end
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require 'posix-spawn'
3
+
4
+ class BacktickTest < Test::Unit::TestCase
5
+ include POSIX::Spawn
6
+
7
+ def test_backtick_simple
8
+ out = `exit`
9
+ assert_equal out, ''
10
+ assert_equal $?.exitstatus, 0
11
+ end
12
+
13
+ def test_backtick_output
14
+ out = `echo 123`
15
+ assert_equal out, "123\n"
16
+ assert_equal $?.exitstatus, 0
17
+ end
18
+
19
+ def test_backtick_failure
20
+ out = `nosuchcmd 2> /dev/null`
21
+ assert_equal out, ''
22
+ assert_equal $?.exitstatus, 127
23
+ end
24
+
25
+ def test_backtick_redirect
26
+ out = `nosuchcmd 2>&1`
27
+ assert_equal out, "/bin/sh: nosuchcmd: command not found\n"
28
+ assert_equal $?.exitstatus, 127
29
+ end
30
+
31
+ def test_backtick_huge
32
+ out = `yes | head -50000`
33
+ assert_equal out.size, 100000
34
+ assert_equal $?.exitstatus, 0
35
+ end
36
+ end
@@ -0,0 +1,107 @@
1
+ require 'test/unit'
2
+ require 'posix-spawn'
3
+
4
+ class ChildTest < Test::Unit::TestCase
5
+ include POSIX::Spawn
6
+
7
+ def test_sanity
8
+ assert_same POSIX::Spawn::Child, Child
9
+ end
10
+
11
+ def test_argv_array_execs
12
+ p = Child.new('printf', '%s %s %s', '1', '2', '3 4')
13
+ assert p.success?
14
+ assert_equal "1 2 3 4", p.out
15
+ end
16
+
17
+ def test_argv_string_uses_sh
18
+ p = Child.new("echo via /bin/sh")
19
+ assert p.success?
20
+ assert_equal "via /bin/sh\n", p.out
21
+ end
22
+
23
+ def test_stdout
24
+ p = Child.new('echo', 'boom')
25
+ assert_equal "boom\n", p.out
26
+ assert_equal "", p.err
27
+ end
28
+
29
+ def test_stderr
30
+ p = Child.new('echo boom 1>&2')
31
+ assert_equal "", p.out
32
+ assert_equal "boom\n", p.err
33
+ end
34
+
35
+ def test_status
36
+ p = Child.new('exit 3')
37
+ assert !p.status.success?
38
+ assert_equal 3, p.status.exitstatus
39
+ end
40
+
41
+ def test_env
42
+ p = Child.new({ 'FOO' => 'BOOYAH' }, 'echo $FOO')
43
+ assert_equal "BOOYAH\n", p.out
44
+ end
45
+
46
+ def test_chdir
47
+ p = Child.new("pwd", :chdir => File.dirname(Dir.pwd))
48
+ assert_equal File.dirname(Dir.pwd) + "\n", p.out
49
+ end
50
+
51
+ def test_input
52
+ input = "HEY NOW\n" * 100_000 # 800K
53
+ p = Child.new('wc', '-l', :input => input)
54
+ assert_equal 100_000, p.out.strip.to_i
55
+ end
56
+
57
+ def test_max
58
+ assert_raise MaximumOutputExceeded do
59
+ Child.new('yes', :max => 100_000)
60
+ end
61
+ end
62
+
63
+ def test_max_with_child_hierarchy
64
+ assert_raise MaximumOutputExceeded do
65
+ Child.new('/bin/sh', '-c', 'yes', :max => 100_000)
66
+ end
67
+ end
68
+
69
+ def test_max_with_stubborn_child
70
+ assert_raise MaximumOutputExceeded do
71
+ Child.new("trap '' TERM; yes", :max => 100_000)
72
+ end
73
+ end
74
+
75
+ def test_timeout
76
+ assert_raise TimeoutExceeded do
77
+ Child.new('sleep', '1', :timeout => 0.05)
78
+ end
79
+ end
80
+
81
+ def test_timeout_with_child_hierarchy
82
+ assert_raise TimeoutExceeded do
83
+ Child.new('/bin/sh', '-c', 'sleep 1', :timeout => 0.05)
84
+ end
85
+ end
86
+
87
+ def test_lots_of_input_and_lots_of_output_at_the_same_time
88
+ input = "stuff on stdin \n" * 1_000
89
+ command = "
90
+ while read line
91
+ do
92
+ echo stuff on stdout;
93
+ echo stuff on stderr 1>&2;
94
+ done
95
+ "
96
+ p = Child.new(command, :input => input)
97
+ assert_equal input.size, p.out.size
98
+ assert_equal input.size, p.err.size
99
+ assert p.success?
100
+ end
101
+
102
+ def test_input_cannot_be_written_due_to_broken_pipe
103
+ input = "1" * 100_000
104
+ p = Child.new('false', :input => input)
105
+ assert !p.success?
106
+ end
107
+ end
@@ -0,0 +1,18 @@
1
+ require 'test/unit'
2
+ require 'posix-spawn'
3
+
4
+ class PopenTest < Test::Unit::TestCase
5
+ include POSIX::Spawn
6
+
7
+ def test_popen4
8
+ pid, i, o, e = popen4("cat")
9
+ i.write "hello world"
10
+ i.close
11
+ ::Process.wait(pid)
12
+
13
+ assert_equal o.read, "hello world"
14
+ assert_equal $?.exitstatus, 0
15
+ ensure
16
+ [i, o, e].each{ |io| io.close rescue nil }
17
+ end
18
+ end
@@ -0,0 +1,360 @@
1
+ require 'test/unit'
2
+ require 'posix-spawn'
3
+
4
+ module SpawnImplementationTests
5
+ def test_spawn_simple
6
+ pid = _spawn('true')
7
+ assert_process_exit_ok pid
8
+ end
9
+
10
+ def test_spawn_with_args
11
+ pid = _spawn('true', 'with', 'some stuff')
12
+ assert_process_exit_ok pid
13
+ end
14
+
15
+ def test_spawn_with_shell
16
+ pid = _spawn('true && exit 13')
17
+ assert_process_exit_status pid, 13
18
+ end
19
+
20
+ def test_spawn_with_cmdname_and_argv0_tuple
21
+ pid = _spawn(['true', 'not-true'], 'some', 'args', 'toooo')
22
+ assert_process_exit_ok pid
23
+ end
24
+
25
+ def test_spawn_with_invalid_argv
26
+ assert_raise ArgumentError do
27
+ _spawn(['echo','b','c','d'])
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Environ
33
+
34
+ def test_spawn_inherit_env
35
+ ENV['PSPAWN'] = 'parent'
36
+ pid = _spawn('test "$PSPAWN" = "parent"')
37
+ assert_process_exit_ok pid
38
+ ensure
39
+ ENV.delete('PSPAWN')
40
+ end
41
+
42
+ def test_spawn_clean_env
43
+ ENV['PSPAWN'] = 'parent'
44
+ pid = _spawn({'TEMP'=>'child'}, 'test -z "$PSPAWN" && test "$TEMP" = "child"', :unsetenv_others => true)
45
+ assert_process_exit_ok pid
46
+ ensure
47
+ ENV.delete('PSPAWN')
48
+ end
49
+
50
+ def test_spawn_set_env
51
+ ENV['PSPAWN'] = 'parent'
52
+ pid = _spawn({'PSPAWN'=>'child'}, 'test "$PSPAWN" = "child"')
53
+ assert_process_exit_ok pid
54
+ ensure
55
+ ENV.delete('PSPAWN')
56
+ end
57
+
58
+ def test_spawn_unset_env
59
+ ENV['PSPAWN'] = 'parent'
60
+ pid = _spawn({'PSPAWN'=>nil}, 'test -z "$PSPAWN"')
61
+ assert_process_exit_ok pid
62
+ ensure
63
+ ENV.delete('PSPAWN')
64
+ end
65
+
66
+ ##
67
+ # FD => :close options
68
+
69
+ def test_sanity_of_checking_clone_with_sh
70
+ rd, wr = IO.pipe
71
+ pid = _spawn("exec 2>/dev/null 100<&#{rd.to_i} || exit 1", rd => rd)
72
+ assert_process_exit_status pid, 0
73
+ ensure
74
+ [rd, wr].each { |fd| fd.close rescue nil }
75
+ end
76
+
77
+ def test_spawn_close_option_with_symbolic_standard_stream_names
78
+ pid = _spawn('exec 2>/dev/null 100<&0 || exit 1', :in => :close)
79
+ assert_process_exit_status pid, 1
80
+
81
+ pid = _spawn('exec 2>/dev/null 101>&1 102>&2 || exit 1',
82
+ :out => :close, :err => :close)
83
+ assert_process_exit_status pid, 1
84
+ end
85
+
86
+ def test_spawn_close_on_standard_stream_io_object
87
+ pid = _spawn('exec 2>/dev/null 100<&0 || exit 1', STDIN => :close)
88
+ assert_process_exit_status pid, 1
89
+
90
+ pid = _spawn('exec 2>/dev/null 101>&1 102>&2 || exit 1',
91
+ STDOUT => :close, STDOUT => :close)
92
+ assert_process_exit_status pid, 1
93
+ end
94
+
95
+ def test_spawn_close_option_with_fd_number
96
+ rd, wr = IO.pipe
97
+ pid = _spawn("exec 2>/dev/null 100<&#{rd.to_i} || exit 1", rd.to_i => :close)
98
+ assert_process_exit_status pid, 1
99
+
100
+ assert !rd.closed?
101
+ assert !wr.closed?
102
+ ensure
103
+ [rd, wr].each { |fd| fd.close rescue nil }
104
+ end
105
+
106
+ def test_spawn_close_option_with_io_object
107
+ rd, wr = IO.pipe
108
+ pid = _spawn("exec 2>/dev/null 100<&#{rd.to_i} || exit 1", rd => :close)
109
+ assert_process_exit_status pid, 1
110
+
111
+ assert !rd.closed?
112
+ assert !wr.closed?
113
+ ensure
114
+ [rd, wr].each { |fd| fd.close rescue nil }
115
+ end
116
+
117
+ def test_spawn_close_invalid_fd_raises_exception
118
+ pid = _spawn("echo", "hiya", 250 => :close)
119
+ assert_process_exit_status pid, 127
120
+ rescue Errno::EBADF
121
+ # this happens on darwin only. GNU does spawn and exits 127.
122
+ end
123
+
124
+ def test_spawn_closing_multiple_fds_with_array_keys
125
+ rd, wr = IO.pipe
126
+ pid = _spawn("exec 2>/dev/null 101>&#{wr.to_i} || exit 1", [rd, wr, :out] => :close)
127
+ assert_process_exit_status pid, 1
128
+ ensure
129
+ [rd, wr].each { |fd| fd.close rescue nil }
130
+ end
131
+
132
+ ##
133
+ # FD => FD options
134
+
135
+ def test_spawn_redirect_fds_with_symbolic_names_and_io_objects
136
+ rd, wr = IO.pipe
137
+ pid = _spawn("echo", "hello world", :out => wr, rd => :close)
138
+ wr.close
139
+ output = rd.read
140
+ assert_process_exit_ok pid
141
+ assert_equal "hello world\n", output
142
+ ensure
143
+ [rd, wr].each { |fd| fd.close rescue nil }
144
+ end
145
+
146
+ def test_spawn_redirect_fds_with_fd_numbers
147
+ rd, wr = IO.pipe
148
+ pid = _spawn("echo", "hello world", 1 => wr.fileno, rd.fileno => :close)
149
+ wr.close
150
+ output = rd.read
151
+ assert_process_exit_ok pid
152
+ assert_equal "hello world\n", output
153
+ ensure
154
+ [rd, wr].each { |fd| fd.close rescue nil }
155
+ end
156
+
157
+ def test_spawn_redirect_invalid_fds_raises_exception
158
+ pid = _spawn("echo", "hiya", 250 => 3)
159
+ assert_process_exit_status pid, 127
160
+ rescue Errno::EBADF
161
+ # this happens on darwin only. GNU does spawn and exits 127.
162
+ end
163
+
164
+ def test_spawn_redirect_stderr_and_stdout_to_same_fd
165
+ rd, wr = IO.pipe
166
+ pid = _spawn("echo hello world 1>&2", :err => wr, :out => wr, rd => :close)
167
+ wr.close
168
+ output = rd.read
169
+ assert_process_exit_ok pid
170
+ assert_equal "hello world\n", output
171
+ ensure
172
+ [rd, wr].each { |fd| fd.close rescue nil }
173
+ end
174
+
175
+ def test_spawn_does_not_close_fd_when_redirecting
176
+ pid = _spawn("exec 2>&1", :err => :out)
177
+ assert_process_exit_ok pid
178
+ end
179
+
180
+ # Ruby 1.9 Process::spawn closes all fds by default. To keep an fd open, you
181
+ # have to pass it explicitly as fd => fd.
182
+ def test_explicitly_passing_an_fd_as_open
183
+ rd, wr = IO.pipe
184
+ pid = _spawn("exec 101>&#{wr.to_i} || exit 1", wr => wr)
185
+ assert_process_exit_ok pid
186
+ ensure
187
+ [rd, wr].each { |fd| fd.close rescue nil }
188
+ end
189
+
190
+ ##
191
+ # FD => file options
192
+
193
+ def test_spawn_redirect_fd_to_file_with_symbolic_name
194
+ file = File.expand_path('../test-output', __FILE__)
195
+ text = 'redirect_fd_to_file_with_symbolic_name'
196
+ pid = _spawn('echo', text, :out => file)
197
+ assert_process_exit_ok pid
198
+ assert File.exist?(file)
199
+ assert_equal "#{text}\n", File.read(file)
200
+ ensure
201
+ File.unlink(file) rescue nil
202
+ end
203
+
204
+ def test_spawn_redirect_fd_to_file_with_fd_number
205
+ file = File.expand_path('../test-output', __FILE__)
206
+ text = 'redirect_fd_to_file_with_fd_number'
207
+ pid = _spawn('echo', text, 1 => file)
208
+ assert_process_exit_ok pid
209
+ assert File.exist?(file)
210
+ assert_equal "#{text}\n", File.read(file)
211
+ ensure
212
+ File.unlink(file) rescue nil
213
+ end
214
+
215
+ def test_spawn_redirect_fd_to_file_with_io_object
216
+ file = File.expand_path('../test-output', __FILE__)
217
+ text = 'redirect_fd_to_file_with_io_object'
218
+ pid = _spawn('echo', text, STDOUT => file)
219
+ assert_process_exit_ok pid
220
+ assert File.exist?(file)
221
+ assert_equal "#{text}\n", File.read(file)
222
+ ensure
223
+ File.unlink(file) rescue nil
224
+ end
225
+
226
+ def test_spawn_redirect_fd_from_file_with_symbolic_name
227
+ file = File.expand_path('../test-input', __FILE__)
228
+ text = 'redirect_fd_from_file_with_symbolic_name'
229
+ File.open(file, 'w') { |fd| fd.write(text) }
230
+
231
+ pid = _spawn(%Q{test "$(cat)" = "#{text}"}, :in => file)
232
+ assert_process_exit_ok pid
233
+ ensure
234
+ File.unlink(file) rescue nil
235
+ end
236
+
237
+ def test_spawn_redirect_fd_from_file_with_fd_number
238
+ file = File.expand_path('../test-input', __FILE__)
239
+ text = 'redirect_fd_from_file_with_fd_number'
240
+ File.open(file, 'w') { |fd| fd.write(text) }
241
+
242
+ pid = _spawn(%Q{test "$(cat)" = "#{text}"}, 0 => file)
243
+ assert_process_exit_ok pid
244
+ ensure
245
+ File.unlink(file) rescue nil
246
+ end
247
+
248
+ def test_spawn_redirect_fd_from_file_with_io_object
249
+ file = File.expand_path('../test-input', __FILE__)
250
+ text = 'redirect_fd_from_file_with_io_object'
251
+ File.open(file, 'w') { |fd| fd.write(text) }
252
+
253
+ pid = _spawn(%Q{test "$(cat)" = "#{text}"}, STDIN => file)
254
+ assert_process_exit_ok pid
255
+ ensure
256
+ File.unlink(file) rescue nil
257
+ end
258
+
259
+ def test_spawn_redirect_fd_to_file_with_symbolic_name_and_flags
260
+ file = File.expand_path('../test-output', __FILE__)
261
+ text = 'redirect_fd_to_file_with_symbolic_name'
262
+ 5.times do
263
+ pid = _spawn('echo', text, :out => [file, 'a'])
264
+ assert_process_exit_ok pid
265
+ end
266
+ assert File.exist?(file)
267
+ assert_equal "#{text}\n" * 5, File.read(file)
268
+ ensure
269
+ File.unlink(file) rescue nil
270
+ end
271
+
272
+ ##
273
+ # Exceptions
274
+
275
+ def test_spawn_raises_exception_on_unsupported_options
276
+ exception = nil
277
+
278
+ assert_raise ArgumentError do
279
+ begin
280
+ _spawn('echo howdy', :out => '/dev/null', :oops => 'blaahh')
281
+ rescue Exception => e
282
+ exception = e
283
+ raise e
284
+ end
285
+ end
286
+
287
+ assert_match /oops/, exception.message
288
+ end
289
+
290
+ ##
291
+ # Assertion Helpers
292
+
293
+ def assert_process_exit_ok(pid)
294
+ assert_process_exit_status pid, 0
295
+ end
296
+
297
+ def assert_process_exit_status(pid, status)
298
+ assert pid.to_i > 0, "pid [#{pid}] should be > 0"
299
+ chpid = ::Process.wait(pid)
300
+ assert_equal chpid, pid
301
+ assert_equal status, $?.exitstatus
302
+ end
303
+ end
304
+
305
+ class SpawnTest < Test::Unit::TestCase
306
+ include POSIX::Spawn
307
+
308
+ def test_spawn_methods_exposed_at_module_level
309
+ assert POSIX::Spawn.respond_to?(:pspawn)
310
+ assert POSIX::Spawn.respond_to?(:_pspawn)
311
+ end
312
+
313
+ ##
314
+ # Options Preprocessing
315
+
316
+ def test_extract_process_spawn_arguments_with_options
317
+ assert_equal [{}, [['echo', 'echo'], 'hello', 'world'], {:err => :close}],
318
+ extract_process_spawn_arguments('echo', 'hello', 'world', :err => :close)
319
+ end
320
+
321
+ def test_extract_process_spawn_arguments_with_options_and_env
322
+ options = {:err => :close}
323
+ env = {'X' => 'Y'}
324
+ assert_equal [env, [['echo', 'echo'], 'hello world'], options],
325
+ extract_process_spawn_arguments(env, 'echo', 'hello world', options)
326
+ end
327
+
328
+ def test_extract_process_spawn_arguments_with_shell_command
329
+ assert_equal [{}, [['/bin/sh', '/bin/sh'], '-c', 'echo hello world'], {}],
330
+ extract_process_spawn_arguments('echo hello world')
331
+ end
332
+
333
+ def test_extract_process_spawn_arguments_with_special_cmdname_argv_tuple
334
+ assert_equal [{}, [['echo', 'fuuu'], 'hello world'], {}],
335
+ extract_process_spawn_arguments(['echo', 'fuuu'], 'hello world')
336
+ end
337
+ end
338
+
339
+ class PosixSpawnTest < Test::Unit::TestCase
340
+ include SpawnImplementationTests
341
+ def _spawn(*argv)
342
+ POSIX::Spawn.pspawn(*argv)
343
+ end
344
+ end
345
+
346
+ class ForkSpawnTest < Test::Unit::TestCase
347
+ include SpawnImplementationTests
348
+ def _spawn(*argv)
349
+ POSIX::Spawn.fspawn(*argv)
350
+ end
351
+ end
352
+
353
+ if ::Process::respond_to?(:spawn)
354
+ class NativeSpawnTest < Test::Unit::TestCase
355
+ include SpawnImplementationTests
356
+ def _spawn(*argv)
357
+ ::Process.spawn(*argv)
358
+ end
359
+ end
360
+ end