tork 15.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/HISTORY.markdown +962 -0
- data/LICENSE +21 -0
- data/README.markdown +305 -0
- data/Rakefile +2 -0
- data/bin/tork +105 -0
- data/bin/tork-driver +86 -0
- data/bin/tork-herald +47 -0
- data/bin/tork-master +87 -0
- data/lib/tork/client.rb +34 -0
- data/lib/tork/config.rb +83 -0
- data/lib/tork/config/parallel_tests.rb +7 -0
- data/lib/tork/config/rails.rb +43 -0
- data/lib/tork/driver.rb +136 -0
- data/lib/tork/master.rb +99 -0
- data/lib/tork/server.rb +38 -0
- data/lib/tork/version.rb +3 -0
- data/man/man1/tork-driver.1 +70 -0
- data/man/man1/tork-herald.1 +22 -0
- data/man/man1/tork-master.1 +61 -0
- data/man/man1/tork.1 +27 -0
- data/tork.gemspec +23 -0
- metadata +138 -0
data/bin/tork-herald
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
=begin
|
3
|
+
|
4
|
+
TORK-HERALD 1 2012-01-23 15.0.0
|
5
|
+
==============================================================================
|
6
|
+
|
7
|
+
NAME
|
8
|
+
------------------------------------------------------------------------------
|
9
|
+
|
10
|
+
tork-herald - reports modified files
|
11
|
+
|
12
|
+
SYNOPSIS
|
13
|
+
------------------------------------------------------------------------------
|
14
|
+
|
15
|
+
`tork-herald` [*OPTION*]...
|
16
|
+
|
17
|
+
DESCRIPTION
|
18
|
+
------------------------------------------------------------------------------
|
19
|
+
|
20
|
+
This program monitors the current working directory and prints relative paths
|
21
|
+
of modified files, one per line, to the standard output stream.
|
22
|
+
|
23
|
+
OPTIONS
|
24
|
+
------------------------------------------------------------------------------
|
25
|
+
|
26
|
+
`-h`, `--help`
|
27
|
+
Display this help manual using man(1).
|
28
|
+
|
29
|
+
SEE ALSO
|
30
|
+
------------------------------------------------------------------------------
|
31
|
+
|
32
|
+
tork(1), tork-driver(1), tork-master(1), tork-herald(1)
|
33
|
+
|
34
|
+
=end =========================================================================
|
35
|
+
|
36
|
+
$0 = File.basename(__FILE__) # for easier indentification in ps(1) output
|
37
|
+
|
38
|
+
require 'binman'
|
39
|
+
BinMan.help
|
40
|
+
|
41
|
+
require 'guard'
|
42
|
+
require 'guard/listener'
|
43
|
+
|
44
|
+
listener = Guard::Listener.select_and_init(:watch_all_modifications => true)
|
45
|
+
listener.on_change {|files| puts files }
|
46
|
+
STDOUT.sync = true # don't buffer puts()
|
47
|
+
listener.start
|
data/bin/tork-master
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
=begin
|
3
|
+
|
4
|
+
TORK-MASTER 1 2012-01-23 15.0.0
|
5
|
+
==============================================================================
|
6
|
+
|
7
|
+
NAME
|
8
|
+
------------------------------------------------------------------------------
|
9
|
+
|
10
|
+
tork-master - absorbs overhead and runs tests
|
11
|
+
|
12
|
+
SYNOPSIS
|
13
|
+
------------------------------------------------------------------------------
|
14
|
+
|
15
|
+
`tork-master` [*OPTION*]...
|
16
|
+
|
17
|
+
DESCRIPTION
|
18
|
+
------------------------------------------------------------------------------
|
19
|
+
|
20
|
+
This program reads the following single-line commands (JSON arrays) from its
|
21
|
+
standard input stream and performs the respective actions as described below.
|
22
|
+
|
23
|
+
`["load",` *paths*`,` *files*`]`
|
24
|
+
Adds the given array of *paths* to Ruby's $LOAD_PATH, loads the given array
|
25
|
+
of *files* after removing their ".rb" file extension if present, and prints
|
26
|
+
the given command line to the standard output stream.
|
27
|
+
|
28
|
+
`["test",` *test_file*`,` *test_names*`]`
|
29
|
+
Runs the given *test_file* in a forked child process while instructing your
|
30
|
+
chosen unit testing framework (loaded by your test execution overhead) to
|
31
|
+
only run those tests that are named in the given array of *test_names*.
|
32
|
+
|
33
|
+
Prints the given command line to the standard output stream immediately
|
34
|
+
after forking the child process.
|
35
|
+
|
36
|
+
Prints the given command line, modified with `"pass"` (if the test passed)
|
37
|
+
or `"fail"` (if the test failed) in place of `"test"`, to the standard
|
38
|
+
output stream after the forked child process finishes.
|
39
|
+
|
40
|
+
The standard output and error streams of the forked child process are
|
41
|
+
redirected to a file whose path and name are the same as that of the test
|
42
|
+
file being run by the forked child process but with ".log" appended.
|
43
|
+
|
44
|
+
`["stop"]`
|
45
|
+
Stops all tests that are currently running and prints the given command line
|
46
|
+
to the standard output stream.
|
47
|
+
|
48
|
+
`["quit"]`
|
49
|
+
Stops all tests that are currently running and exits.
|
50
|
+
|
51
|
+
OPTIONS
|
52
|
+
------------------------------------------------------------------------------
|
53
|
+
|
54
|
+
`-h`, `--help`
|
55
|
+
Display this help manual using man(1).
|
56
|
+
|
57
|
+
FILES
|
58
|
+
------------------------------------------------------------------------------
|
59
|
+
|
60
|
+
*.tork.rb*
|
61
|
+
Optional Ruby script for configuring tork(1).
|
62
|
+
|
63
|
+
ENVIRONMENT
|
64
|
+
------------------------------------------------------------------------------
|
65
|
+
|
66
|
+
`TORK_CONFIGS`
|
67
|
+
A single-line JSON array containing paths to actual files or names of
|
68
|
+
helper libraries in the tork/config/ namespace of Ruby's load path.
|
69
|
+
These configuration files are loaded just before *.tork.rb* is loaded.
|
70
|
+
|
71
|
+
SEE ALSO
|
72
|
+
------------------------------------------------------------------------------
|
73
|
+
|
74
|
+
tork(1), tork-driver(1), tork-master(1), tork-herald(1)
|
75
|
+
|
76
|
+
=end =========================================================================
|
77
|
+
|
78
|
+
$0 = File.basename(__FILE__) # for easier indentification in ps(1) output
|
79
|
+
|
80
|
+
require 'binman'
|
81
|
+
BinMan.help
|
82
|
+
|
83
|
+
require 'tork/master'
|
84
|
+
Tork::Master.loop
|
85
|
+
|
86
|
+
Process.waitall
|
87
|
+
raise SystemExit # prevent empty test suite from running in the master process
|
data/lib/tork/client.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tork
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class Receiver < Thread
|
5
|
+
def initialize *popen_args
|
6
|
+
(@io = IO.popen(*popen_args)).sync = true
|
7
|
+
super() { loop { yield @io.gets } }
|
8
|
+
end
|
9
|
+
|
10
|
+
def quit
|
11
|
+
kill # stop receive loop
|
12
|
+
Process.kill :SIGTERM, @io.pid
|
13
|
+
Process.wait @io.pid # reap zombie
|
14
|
+
@io.close # prevent further I/O
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Transceiver < Receiver
|
19
|
+
def initialize *popen_args
|
20
|
+
@io_write_lock = Mutex.new
|
21
|
+
popen_args[1] = 'w+'
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def send command
|
26
|
+
@io_write_lock.synchronize do
|
27
|
+
warn "#{caller[2]} SEND #{command.inspect}" if $DEBUG
|
28
|
+
@io.puts JSON.dump(command)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/tork/config.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Tork
|
4
|
+
_user_config_file = '.tork.rb'
|
5
|
+
|
6
|
+
Config = OpenStruct.new
|
7
|
+
|
8
|
+
#---------------------------------------------------------------------------
|
9
|
+
# defaults
|
10
|
+
#---------------------------------------------------------------------------
|
11
|
+
|
12
|
+
Config.max_forked_workers = [
|
13
|
+
# http://stackoverflow.com/questions/891537#6420817
|
14
|
+
'fgrep -c processor /proc/cpuinfo', # Linux
|
15
|
+
'sysctl -n hw.ncpu', # BSD
|
16
|
+
'hwprefs cpu_count', # Darwin 9
|
17
|
+
'hwprefs thread_count', # Darwin 10
|
18
|
+
].map {|cmd| `#{cmd} 2>/dev/null`.to_i }.push(1).max
|
19
|
+
|
20
|
+
Config.overhead_load_paths = ['lib', 'test', 'spec']
|
21
|
+
|
22
|
+
Config.overhead_file_globs = ['{test,spec}/{test,spec}_helper.rb']
|
23
|
+
|
24
|
+
Config.reabsorb_file_greps = [/^#{Regexp.quote(_user_config_file)}$/,
|
25
|
+
%r<(test|spec)/\1_helper\.rb>]
|
26
|
+
|
27
|
+
Config.all_test_file_globs = ['{test,spec}/**/*_{test,spec}.rb']
|
28
|
+
|
29
|
+
Config.test_file_globbers = {
|
30
|
+
# source files that correspond to test files
|
31
|
+
%r<^lib/.+\.rb$> => lambda do |path|
|
32
|
+
base = File.basename(path, '.rb')
|
33
|
+
"{test,spec}/**/#{base}_{test,spec}.rb"
|
34
|
+
end,
|
35
|
+
|
36
|
+
# the actual test files themselves
|
37
|
+
%r<^(test|spec)/.+_\1\.rb$> => lambda {|path| path }
|
38
|
+
}
|
39
|
+
|
40
|
+
Config.test_name_extractor = lambda do |line|
|
41
|
+
case line
|
42
|
+
when /^\s*def\s+test_(\w+)/ then $1
|
43
|
+
when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/ then $3
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Config.before_fork_hooks = []
|
48
|
+
|
49
|
+
Config.after_fork_hooks = [
|
50
|
+
# tell testing framework to only run the named tests inside the test file
|
51
|
+
lambda do |worker_number, log_file, test_file, test_names|
|
52
|
+
unless test_names.empty?
|
53
|
+
case File.basename(test_file)
|
54
|
+
when /(\b|_)test(\b|_)/ # Test::Unit
|
55
|
+
ARGV.push '--name', "/(?i:#{
|
56
|
+
test_names.map do |name|
|
57
|
+
# elide string interpolation and invalid method name characters
|
58
|
+
name.gsub(/\#\{.*?\}/, ' ').strip.gsub(/\W+/, '.*')
|
59
|
+
end.join('|')
|
60
|
+
})/"
|
61
|
+
when /(\b|_)spec(\b|_)/ # RSpec
|
62
|
+
test_names.each {|name| ARGV.push '--example', name }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
]
|
67
|
+
|
68
|
+
#---------------------------------------------------------------------------
|
69
|
+
# overrides
|
70
|
+
#---------------------------------------------------------------------------
|
71
|
+
|
72
|
+
if ENV.key? 'TORK_CONFIGS'
|
73
|
+
JSON.load(ENV['TORK_CONFIGS']).each do |config|
|
74
|
+
if File.exist? config
|
75
|
+
load File.expand_path(config)
|
76
|
+
else
|
77
|
+
require "tork/config/#{config}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
load _user_config_file if File.exist? _user_config_file
|
83
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'tork/config'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
|
4
|
+
Tork::Config.reabsorb_file_greps.push(
|
5
|
+
%r<^config/.+\.(rb|yml)$>,
|
6
|
+
%r<^db/schema\.rb$>,
|
7
|
+
%r<^Gemfile\.lock$>
|
8
|
+
)
|
9
|
+
|
10
|
+
Tork::Config.test_file_globbers[%r<^(app|lib|test|spec)/.+\.rb$>] =
|
11
|
+
lambda do |path|
|
12
|
+
base = File.basename(path, '.rb')
|
13
|
+
poly = ActiveSupport::Inflector.pluralize(base)
|
14
|
+
"{test,spec}/**/{#{base},#{poly}_*}_{test,spec}.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
Tork::Config.test_file_globbers[%r<^(test|spec)/factories/.+_factory\.rb$>] =
|
18
|
+
lambda do |path|
|
19
|
+
base = File.basename(path, '_factory.rb')
|
20
|
+
poly = ActiveSupport::Inflector.pluralize(base)
|
21
|
+
"{test,spec}/**/{#{base},#{poly}_*}_{test,spec}.rb"
|
22
|
+
end
|
23
|
+
|
24
|
+
Tork::Config.after_fork_hooks << proc do
|
25
|
+
unless ActiveRecord::Base.connection_config[:database] == ':memory:'
|
26
|
+
ActiveRecord::Base.connection.reconnect!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
require 'rails/railtie'
|
32
|
+
Class.new Rails::Railtie do
|
33
|
+
config.before_initialize do |app|
|
34
|
+
if app.config.cache_classes
|
35
|
+
warn "tork/config/rails: Setting #{app.class}.config.cache_classes = false"
|
36
|
+
app.config.cache_classes = false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
warn "tork/config/rails: Railtie not available; please manually set:\n\t"\
|
42
|
+
"config.cache_classes = false"
|
43
|
+
end
|
data/lib/tork/driver.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'diff/lcs'
|
3
|
+
require 'tork/client'
|
4
|
+
require 'tork/server'
|
5
|
+
require 'tork/config'
|
6
|
+
|
7
|
+
module Tork
|
8
|
+
module Driver
|
9
|
+
|
10
|
+
extend Server
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def run_all_test_files
|
14
|
+
run_test_files Dir[*Config.all_test_file_globs]
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop_running_test_files
|
18
|
+
@master.send [:stop]
|
19
|
+
@running_test_files.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
def rerun_passed_test_files
|
23
|
+
run_test_files @passed_test_files
|
24
|
+
end
|
25
|
+
|
26
|
+
def rerun_failed_test_files
|
27
|
+
run_test_files @failed_test_files
|
28
|
+
end
|
29
|
+
|
30
|
+
def reabsorb_overhead_files
|
31
|
+
@master.quit if defined? @master
|
32
|
+
|
33
|
+
@master = Client::Transceiver.new('tork-master') do |line|
|
34
|
+
event, file, tests = JSON.load(line)
|
35
|
+
|
36
|
+
case event.to_sym
|
37
|
+
when :test
|
38
|
+
@waiting_test_files.delete file
|
39
|
+
@running_test_files.push file
|
40
|
+
|
41
|
+
when :pass
|
42
|
+
@running_test_files.delete file
|
43
|
+
@failed_test_files.delete file
|
44
|
+
if tests.empty? and not @passed_test_files.include? file
|
45
|
+
@passed_test_files.push file
|
46
|
+
end
|
47
|
+
|
48
|
+
when :fail
|
49
|
+
@running_test_files.delete file
|
50
|
+
@passed_test_files.delete file
|
51
|
+
@failed_test_files.push file unless @failed_test_files.include? file
|
52
|
+
end
|
53
|
+
|
54
|
+
@client.print line
|
55
|
+
end
|
56
|
+
|
57
|
+
@master.send [:load, Config.overhead_load_paths,
|
58
|
+
Dir[*Config.overhead_file_globs]]
|
59
|
+
|
60
|
+
rerun_running_test_files
|
61
|
+
end
|
62
|
+
|
63
|
+
def loop
|
64
|
+
reabsorb_overhead_files
|
65
|
+
|
66
|
+
@herald = Client::Receiver.new('tork-herald') do |line|
|
67
|
+
changed_file = line.chomp
|
68
|
+
warn "tork-driver: herald: #{changed_file}" if $DEBUG
|
69
|
+
|
70
|
+
# find and run the tests that correspond to the changed file
|
71
|
+
Config.test_file_globbers.each do |regexp, globber|
|
72
|
+
if regexp =~ changed_file and glob = globber.call(changed_file)
|
73
|
+
run_test_files Dir[glob]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# reabsorb text execution overhead if overhead files changed
|
78
|
+
if Config.reabsorb_file_greps.any? {|r| r =~ changed_file }
|
79
|
+
@client.puts JSON.dump([:over, changed_file])
|
80
|
+
# NOTE: new thread because reabsorb_overhead_files will kill this one
|
81
|
+
Thread.new { reabsorb_overhead_files }.join
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
super
|
86
|
+
|
87
|
+
@herald.quit
|
88
|
+
@master.quit
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
@waiting_test_files = []
|
94
|
+
@running_test_files = []
|
95
|
+
@passed_test_files = []
|
96
|
+
@failed_test_files = []
|
97
|
+
|
98
|
+
def rerun_running_test_files
|
99
|
+
run_test_files @running_test_files
|
100
|
+
end
|
101
|
+
|
102
|
+
def run_test_files files
|
103
|
+
files.each {|f| run_test_file f }
|
104
|
+
end
|
105
|
+
|
106
|
+
def run_test_file file
|
107
|
+
if File.exist? file and not @waiting_test_files.include? file
|
108
|
+
@waiting_test_files.push file
|
109
|
+
@master.send [:test, file, find_changed_test_names(file)]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
@lines_by_file = {}
|
114
|
+
|
115
|
+
def find_changed_test_names test_file
|
116
|
+
# cache the contents of the test file for diffing below
|
117
|
+
new_lines = File.readlines(test_file)
|
118
|
+
old_lines = @lines_by_file[test_file] || new_lines
|
119
|
+
@lines_by_file[test_file] = new_lines
|
120
|
+
|
121
|
+
# find which tests have changed inside the given test file
|
122
|
+
Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
|
123
|
+
catch :found do
|
124
|
+
# search backwards from the line that changed up to
|
125
|
+
# the first line in the file for test definitions
|
126
|
+
change.position.downto(0) do |i|
|
127
|
+
if test_name = Config.test_name_extractor.call(new_lines[i])
|
128
|
+
throw :found, test_name
|
129
|
+
end
|
130
|
+
end; nil # prevent unsuccessful search from returning an integer
|
131
|
+
end
|
132
|
+
end.compact.uniq
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
data/lib/tork/master.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'tork/server'
|
3
|
+
require 'tork/config'
|
4
|
+
|
5
|
+
module Tork
|
6
|
+
module Master
|
7
|
+
|
8
|
+
extend Server
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def load paths, files
|
12
|
+
$LOAD_PATH.unshift(*paths)
|
13
|
+
|
14
|
+
files.each do |file|
|
15
|
+
branch, leaf = File.split(file)
|
16
|
+
file = leaf if paths.include? branch
|
17
|
+
require file.sub(/\.rb$/, '')
|
18
|
+
end
|
19
|
+
|
20
|
+
@client.print @command_line
|
21
|
+
end
|
22
|
+
|
23
|
+
def test test_file, test_names
|
24
|
+
# throttle forking rate to meet the maximum concurrent workers limit
|
25
|
+
sleep 1 until @command_by_worker_pid.size < Config.max_forked_workers
|
26
|
+
|
27
|
+
log_file = test_file + '.log'
|
28
|
+
worker_number = @worker_number_pool.shift
|
29
|
+
|
30
|
+
Config.before_fork_hooks.each do |hook|
|
31
|
+
hook.call worker_number, log_file, test_file, test_names
|
32
|
+
end
|
33
|
+
|
34
|
+
worker_pid = fork do
|
35
|
+
# make the process title Test::Unit friendly and ps(1) searchable
|
36
|
+
$0 = "tork-worker[#{worker_number}] #{test_file}"
|
37
|
+
|
38
|
+
# detach worker process from master process' group for kill -pgrp
|
39
|
+
Process.setsid
|
40
|
+
|
41
|
+
# detach worker process from master process' standard input stream
|
42
|
+
STDIN.reopen IO.pipe.first
|
43
|
+
|
44
|
+
# capture test output in log file because tests are run in parallel
|
45
|
+
# which makes it difficult to understand interleaved output thereof
|
46
|
+
STDERR.reopen(STDOUT.reopen(log_file, 'w')).sync = true
|
47
|
+
|
48
|
+
Config.after_fork_hooks.each do |hook|
|
49
|
+
hook.call worker_number, log_file, test_file, test_names
|
50
|
+
end
|
51
|
+
|
52
|
+
# after loading the user's test file, the at_exit() hook of the user's
|
53
|
+
# testing framework will take care of running the tests and reflecting
|
54
|
+
# any failures in the worker process' exit status, which will then be
|
55
|
+
# handled by the SIGCHLD trap registered in the master process (below)
|
56
|
+
Kernel.load test_file
|
57
|
+
end
|
58
|
+
|
59
|
+
@command_by_worker_pid[worker_pid] = @command.push(worker_number)
|
60
|
+
@client.print @command_line
|
61
|
+
end
|
62
|
+
|
63
|
+
def stop
|
64
|
+
# NOTE: the SIGCHLD handler will reap these killed worker processes
|
65
|
+
Process.kill :SIGTERM, *@command_by_worker_pid.keys.map {|pid| -pid }
|
66
|
+
rescue ArgumentError, SystemCallError
|
67
|
+
# some workers might have already exited before we sent them the signal
|
68
|
+
end
|
69
|
+
|
70
|
+
def loop
|
71
|
+
super
|
72
|
+
stop
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
@worker_number_pool = (0 ... Config.max_forked_workers).to_a
|
78
|
+
@command_by_worker_pid = {}
|
79
|
+
|
80
|
+
# process exited child processes and report finished workers to client
|
81
|
+
trap :SIGCHLD do
|
82
|
+
begin
|
83
|
+
while wait2_array = Process.wait2(-1, Process::WNOHANG)
|
84
|
+
child_pid, child_status = wait2_array
|
85
|
+
if command = @command_by_worker_pid.delete(child_pid)
|
86
|
+
@worker_number_pool.push command.pop
|
87
|
+
command[0] = child_status.success? ? 'pass' : 'fail'
|
88
|
+
@client.puts JSON.dump(command.push(child_status))
|
89
|
+
else
|
90
|
+
warn "tork-master: unknown child exited: #{wait2_array.inspect}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
rescue SystemCallError
|
94
|
+
# raised by wait2() when there are currently no child processes
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|