posix-spawn 0.3.0

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,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