tork 18.2.4 → 19.0.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.
- data/HISTORY.markdown +65 -0
- data/README.markdown +60 -298
- data/bin/tork +99 -62
- data/bin/tork-driver +124 -25
- data/bin/tork-engine +45 -23
- data/bin/tork-herald +5 -5
- data/bin/tork-master +79 -32
- data/bin/tork-notify +70 -0
- data/bin/tork-remote +80 -0
- data/lib/tork/cliapp.rb +73 -0
- data/lib/tork/config.rb +14 -97
- data/lib/tork/config/coverage/master.rb +29 -0
- data/lib/tork/config/coverage/worker.rb +1 -0
- data/lib/tork/config/cucumber/driver.rb +11 -0
- data/lib/tork/config/cucumber/worker.rb +14 -0
- data/lib/tork/config/default/config.rb +5 -0
- data/lib/tork/config/dotlog/onfork.rb +2 -0
- data/lib/tork/config/factory_girl/onfork.rb +2 -0
- data/lib/tork/config/factory_girl/worker.rb +1 -0
- data/lib/tork/config/logdir/onfork.rb +4 -0
- data/lib/tork/config/parallel_tests/worker.rb +4 -0
- data/lib/tork/config/rails/driver.rb +15 -0
- data/lib/tork/config/rails/master.rb +12 -0
- data/lib/tork/config/rails/worker.rb +21 -0
- data/lib/tork/config/spec/driver.rb +17 -0
- data/lib/tork/config/spec/master.rb +3 -0
- data/lib/tork/config/spec/worker.rb +3 -0
- data/lib/tork/config/test/driver.rb +17 -0
- data/lib/tork/config/test/master.rb +3 -0
- data/lib/tork/config/test/worker.rb +23 -0
- data/lib/tork/driver.rb +68 -36
- data/lib/tork/engine.rb +57 -44
- data/lib/tork/master.rb +34 -34
- data/lib/tork/server.rb +118 -17
- data/lib/tork/version.rb +1 -1
- data/man/man1/tork-driver.1 +165 -37
- data/man/man1/tork-engine.1 +50 -32
- data/man/man1/tork-herald.1 +5 -8
- data/man/man1/tork-master.1 +95 -42
- data/man/man1/tork-notify.1 +27 -0
- data/man/man1/tork-remote.1 +38 -0
- data/man/man1/tork.1 +116 -8
- data/tork.gemspec +3 -3
- metadata +36 -25
- data/lib/tork/client.rb +0 -53
- data/lib/tork/config/coverage.rb +0 -40
- data/lib/tork/config/cucumber.rb +0 -32
- data/lib/tork/config/dotlog.rb +0 -8
- data/lib/tork/config/factory_girl.rb +0 -10
- data/lib/tork/config/logdir.rb +0 -10
- data/lib/tork/config/notify.rb +0 -34
- data/lib/tork/config/parallel_tests.rb +0 -9
- data/lib/tork/config/rails.rb +0 -39
data/lib/tork/master.rb
CHANGED
@@ -4,37 +4,44 @@ require 'tork/config'
|
|
4
4
|
module Tork
|
5
5
|
class Master < Server
|
6
6
|
|
7
|
+
# detect the number of CPUs available in the system
|
8
|
+
# http://stackoverflow.com/questions/891537#6420817
|
9
|
+
MAX_CONCURRENT_WORKERS = [
|
10
|
+
'fgrep -c processor /proc/cpuinfo', # Linux
|
11
|
+
'sysctl -n hw.ncpu', # BSD
|
12
|
+
'hwprefs cpu_count', # Darwin 9
|
13
|
+
'hwprefs thread_count', # Darwin 10
|
14
|
+
].
|
15
|
+
map {|cmd| `#{cmd} 2>/dev/null`.to_i }.push(1).max
|
16
|
+
|
7
17
|
def initialize
|
8
18
|
super
|
19
|
+
Tork.config :master
|
20
|
+
send @clients, [:absorb]
|
9
21
|
|
10
|
-
@worker_number_pool = (0 ...
|
22
|
+
@worker_number_pool = (0 ... MAX_CONCURRENT_WORKERS).to_a
|
11
23
|
@command_by_worker_pid = {}
|
12
24
|
end
|
13
25
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
branch, leaf = File.split(file)
|
19
|
-
file = leaf if paths.include? branch
|
20
|
-
require file.sub(/\.rb$/, '')
|
21
|
-
end
|
22
|
-
|
23
|
-
@client.send @command
|
26
|
+
def loop
|
27
|
+
super
|
28
|
+
ensure
|
29
|
+
stop :SIGKILL
|
24
30
|
end
|
25
31
|
|
26
32
|
def test test_file, line_numbers
|
27
|
-
return if @overhead_files.include? test_file
|
28
|
-
|
29
33
|
# throttle forking rate to meet the maximum concurrent workers limit
|
30
|
-
sleep 1 until @command_by_worker_pid.size <
|
34
|
+
sleep 1 until @command_by_worker_pid.size < @worker_number_pool.size
|
31
35
|
|
32
36
|
log_file = test_file + '.log'
|
33
37
|
worker_number = @worker_number_pool.shift
|
38
|
+
@command.push log_file, worker_number
|
34
39
|
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
$tork_test_file = test_file
|
41
|
+
$tork_line_numbers = line_numbers
|
42
|
+
$tork_log_file = log_file
|
43
|
+
$tork_worker_number = worker_number
|
44
|
+
Tork.config :onfork
|
38
45
|
|
39
46
|
worker_pid = fork do
|
40
47
|
# make the process title Test::Unit friendly and ps(1) searchable
|
@@ -50,9 +57,7 @@ class Master < Server
|
|
50
57
|
# which makes it difficult to understand interleaved output thereof
|
51
58
|
STDERR.reopen(STDOUT.reopen(log_file, 'w')).sync = true
|
52
59
|
|
53
|
-
|
54
|
-
hook.call test_file, line_numbers, log_file, worker_number
|
55
|
-
end
|
60
|
+
Tork.config :worker
|
56
61
|
|
57
62
|
# after loading the user's test file, the at_exit() hook of the user's
|
58
63
|
# testing framework will take care of running the tests and reflecting
|
@@ -61,30 +66,25 @@ class Master < Server
|
|
61
66
|
Kernel.load test_file if test_file.end_with? '.rb'
|
62
67
|
end
|
63
68
|
|
64
|
-
@command_by_worker_pid[worker_pid] = @command
|
65
|
-
|
69
|
+
@command_by_worker_pid[worker_pid] = @command
|
70
|
+
send @clients, @command
|
66
71
|
|
67
72
|
# wait for the worker to finish and report its status to the client
|
68
|
-
Thread.new do # the reaping thread
|
69
|
-
|
70
|
-
command = @command_by_worker_pid.delete(
|
73
|
+
Thread.new(worker_pid) do |pid| # the reaping thread
|
74
|
+
status = Process.wait2(pid).last
|
75
|
+
command = @command_by_worker_pid.delete(pid)
|
71
76
|
@worker_number_pool.push command.last
|
72
|
-
command[0] =
|
73
|
-
|
77
|
+
command[0] = status.success? && :pass || :fail
|
78
|
+
send @clients, command.push(status.to_i, status.inspect)
|
74
79
|
end
|
75
80
|
end
|
76
81
|
|
77
|
-
def stop
|
82
|
+
def stop signal=:SIGTERM
|
78
83
|
# the reaping threads registered above will reap these killed workers
|
79
|
-
Process.kill
|
84
|
+
Process.kill signal, *@command_by_worker_pid.keys.map {|pid| -pid }
|
80
85
|
rescue ArgumentError, SystemCallError
|
81
86
|
# some workers might have already exited before we sent them the signal
|
82
87
|
end
|
83
88
|
|
84
|
-
def quit
|
85
|
-
stop
|
86
|
-
super
|
87
|
-
end
|
88
|
-
|
89
89
|
end
|
90
90
|
end
|
data/lib/tork/server.rb
CHANGED
@@ -1,31 +1,132 @@
|
|
1
|
-
require '
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
require 'json'
|
4
|
+
require 'shellwords'
|
2
5
|
|
3
6
|
module Tork
|
4
7
|
class Server
|
5
8
|
|
6
|
-
def
|
7
|
-
|
9
|
+
def self.address program=$0
|
10
|
+
".#{program}.sock"
|
8
11
|
end
|
9
12
|
|
10
|
-
def
|
11
|
-
|
13
|
+
def initialize
|
14
|
+
# only JSON messages are supposed to be emitted on STDOUT
|
15
|
+
# so make puts() in the user code write to STDERR instead
|
16
|
+
@stdout = STDOUT.dup
|
17
|
+
STDOUT.reopen STDERR
|
18
|
+
|
19
|
+
@clients = [STDIN]
|
20
|
+
@servers = []
|
12
21
|
end
|
13
22
|
|
14
23
|
def loop
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
server = UNIXServer.open(Server.address)
|
25
|
+
@servers << server
|
26
|
+
catch :quit do
|
27
|
+
while @clients.include? STDIN
|
28
|
+
IO.select(@servers + @clients).first.each do |stream|
|
29
|
+
@client = stream
|
30
|
+
|
31
|
+
if stream == server
|
32
|
+
@clients << stream.accept
|
33
|
+
|
34
|
+
elsif (stream.eof? rescue true)
|
35
|
+
@clients.delete stream
|
36
|
+
|
37
|
+
elsif @command = hear(stream, stream.gets)
|
38
|
+
recv stream, @command
|
39
|
+
end
|
26
40
|
end
|
27
41
|
end
|
28
|
-
end
|
42
|
+
end
|
43
|
+
ensure
|
44
|
+
# UNIX domain socket files are not deleted automatically upon closing
|
45
|
+
File.delete server.path if server
|
46
|
+
end
|
47
|
+
|
48
|
+
def quit
|
49
|
+
throw :quit
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
JSON_REGEXP = /\A\s*[\[\{]/.freeze
|
55
|
+
|
56
|
+
# On failure to decode the message, warns the sender and returns nil.
|
57
|
+
def hear sender, message
|
58
|
+
if message =~ JSON_REGEXP
|
59
|
+
JSON.load message
|
60
|
+
|
61
|
+
# accept non-JSON "command lines" from clients
|
62
|
+
elsif @clients.include? sender
|
63
|
+
Shellwords.split message
|
64
|
+
|
65
|
+
# forward tell() output from children to clients
|
66
|
+
elsif @servers.include? sender
|
67
|
+
tell @clients, message, false
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
rescue JSON::ParserError => error
|
71
|
+
tell sender, error
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def recv client, command
|
76
|
+
__send__(*command)
|
77
|
+
rescue => error
|
78
|
+
tell client, error
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def send one_or_more_clients, message
|
83
|
+
tell one_or_more_clients, JSON.dump(message), false
|
84
|
+
end
|
85
|
+
|
86
|
+
def tell one_or_more_clients, message, prefix=true
|
87
|
+
if message.kind_of? Exception
|
88
|
+
message = [message.inspect, message.backtrace]
|
89
|
+
end
|
90
|
+
|
91
|
+
if prefix
|
92
|
+
message = Array(message).join("\n").gsub(/^/, "#{$0}: ")
|
93
|
+
end
|
94
|
+
|
95
|
+
targets =
|
96
|
+
if one_or_more_clients.kind_of? IO
|
97
|
+
[one_or_more_clients]
|
98
|
+
else
|
99
|
+
Array(one_or_more_clients)
|
100
|
+
end
|
101
|
+
|
102
|
+
targets.each do |target|
|
103
|
+
target = @stdout if target == STDIN
|
104
|
+
target.puts message
|
105
|
+
target.flush
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def popen command
|
110
|
+
child = IO.popen(command, 'r+')
|
111
|
+
@servers << child
|
112
|
+
child
|
113
|
+
end
|
114
|
+
|
115
|
+
def pclose child
|
116
|
+
return unless @servers.delete child
|
117
|
+
|
118
|
+
# this should be enough to stop programs that use Tork::Server#loop
|
119
|
+
# because their IO.select() loop terminates on the closing of STDIN
|
120
|
+
child.close_write
|
121
|
+
|
122
|
+
# but some programs like tork-herald(1) need to be killed explicitly
|
123
|
+
# because they do not follow this convention of exiting on STDIN close
|
124
|
+
Process.kill :SIGTERM, child.pid
|
125
|
+
Process.waitpid child.pid
|
126
|
+
|
127
|
+
# this will block until the child process has exited so we must kill it
|
128
|
+
# explicitly (as above) to ensure that this program does not hang here
|
129
|
+
child.close_read
|
29
130
|
end
|
30
131
|
|
31
132
|
end
|
data/lib/tork/version.rb
CHANGED
data/man/man1/tork-driver.1
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
.TH TORK\-DRIVER 1 2012\-10\-
|
1
|
+
.TH TORK\-DRIVER 1 2012\-10\-17 19.0.0
|
2
2
|
.SH NAME
|
3
3
|
.PP
|
4
4
|
tork\-driver \- drives
|
@@ -11,61 +11,189 @@ when files change
|
|
11
11
|
.PP
|
12
12
|
This program drives
|
13
13
|
.BR tork-engine (1)
|
14
|
-
|
15
|
-
.BR tork-herald (1)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
.
|
21
|
-
|
14
|
+
when
|
15
|
+
.BR tork-herald (1)
|
16
|
+
reports files changes.
|
17
|
+
.PP
|
18
|
+
This program can be controlled remotely by multiple
|
19
|
+
.BR tork-remote (1)
|
20
|
+
instances.
|
21
|
+
.SS Input
|
22
|
+
.PP
|
23
|
+
This program reads the following commands, which are single\-line JSON arrays,
|
24
|
+
from stdin and performs the actions described respectively.
|
22
25
|
.TP
|
23
26
|
\fB\fC["run_all_test_files"]\fR
|
24
27
|
Runs all test files found within and beneath the current working directory.
|
25
28
|
.TP
|
26
|
-
\fB\fC["reabsorb_overhead_files"]\fR
|
27
|
-
Stops any test files that are currently running, reabsorbs the test
|
28
|
-
execution overhead, and resumes running those interrupted test files.
|
29
|
-
.TP
|
30
29
|
\fI...\fP
|
31
|
-
|
30
|
+
Commands for
|
32
31
|
.BR tork-engine (1)
|
33
|
-
|
32
|
+
are also accepted here.
|
33
|
+
.SS Output
|
34
34
|
.PP
|
35
|
-
|
36
|
-
.
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
instance, which then absorbs the modified test execution overhead into itself.
|
42
|
-
.PP
|
43
|
-
This program emits the following single\-line status messages (JSON arrays) on
|
44
|
-
its standard output stream to provide notifications about its activity:
|
35
|
+
This program prints the following messages, which are single\-line JSON arrays,
|
36
|
+
to stdout.
|
37
|
+
.TP
|
38
|
+
\fB\fC["reabsorb",\fR \fIoverhead_file\fP\fB\fC]\fR
|
39
|
+
Test execution overhead is being reabsorbed because \fIoverhead_file\fP has
|
40
|
+
changed.
|
45
41
|
.TP
|
46
|
-
\
|
47
|
-
|
42
|
+
\fI...\fP
|
43
|
+
Messages from
|
44
|
+
.BR tork-engine (1)
|
45
|
+
and
|
48
46
|
.BR tork-master (1)
|
49
|
-
|
47
|
+
are also reproduced here.
|
50
48
|
.SH OPTIONS
|
51
49
|
.TP
|
52
50
|
\fB\fC-h\fR, \fB\fC--help\fR
|
53
51
|
Show this help manual.
|
54
52
|
.SH FILES
|
55
53
|
.TP
|
56
|
-
\fI.tork.rb\fP
|
57
|
-
Optional Ruby script
|
58
|
-
|
59
|
-
.
|
54
|
+
\fI.tork/config.rb\fP
|
55
|
+
Optional Ruby script that is loaded inside the driver process on startup.
|
56
|
+
It can read and change the \fB\fCENV['TORK_CONFIGS']\fR environment variable.
|
57
|
+
.TP
|
58
|
+
\fB\fC.tork/driver.rb\fR
|
59
|
+
Optional Ruby script that is loaded inside the driver process on startup.
|
60
|
+
It can read and change the following variables.
|
61
|
+
.PP
|
62
|
+
.RS
|
63
|
+
.TP
|
64
|
+
\fB\fCTork::Driver::REABSORB_FILE_GREPS\fR
|
65
|
+
Array of strings or regular expressions that match the paths of overhead
|
66
|
+
files. If any of these equal or match the path of a changed file
|
67
|
+
reported by
|
68
|
+
.BR tork-herald (1),
|
69
|
+
then the test execution overhead will be
|
70
|
+
reabsorbed in
|
71
|
+
.BR tork-master (1).
|
72
|
+
.TP
|
73
|
+
\fB\fCTork::Driver::ALL_TEST_FILE_GLOBS\fR
|
74
|
+
Array of file globbing patterns that describe the set of all test files
|
75
|
+
in your Ruby application.
|
60
76
|
.TP
|
61
|
-
\fB\
|
62
|
-
|
63
|
-
|
64
|
-
|
77
|
+
\fB\fCTork::Driver::TEST_FILE_GLOBBERS\fR
|
78
|
+
Hash that maps (1) a regular expression describing a set of file paths
|
79
|
+
to (2) a lambda function that accepts a \fB\fCMatchData\fR object containing
|
80
|
+
the results of the regular expression matching against the path of a
|
81
|
+
changed file, and yields one or more file globbing patterns (a single
|
82
|
+
string, or an array of strings) that describe a set of test files that
|
83
|
+
need to be run.
|
84
|
+
.IP
|
85
|
+
The results of these functions are recursively expanded (fed back into
|
86
|
+
them) to construct an entire dependency tree of test files that need to
|
87
|
+
be run. For instance, if one function returns a glob that yields files
|
88
|
+
matched by another function, then that second function will be called to
|
89
|
+
glob more test files. This process repeats until all dependent test
|
90
|
+
files have been accounted for.
|
91
|
+
.PP
|
92
|
+
.RS
|
93
|
+
\s+2\fBSingle glob expansion\fP\s-2
|
94
|
+
.PP
|
95
|
+
For example, if test files had the same names as their source files
|
96
|
+
followed by an underscore and the file name in reverse like this:
|
97
|
+
.RS
|
98
|
+
.IP \(bu 2
|
99
|
+
lib/hello.rb => test/hello_olleh.rb
|
100
|
+
.IP \(bu 2
|
101
|
+
app/world.rb => spec/world_ldrow.rb
|
102
|
+
.RE
|
103
|
+
.PP
|
104
|
+
Then you would add the following to your configuration file:
|
105
|
+
.PP
|
106
|
+
.RS
|
107
|
+
.nf
|
108
|
+
Tork::Driver::TEST_FILE_GLOBBERS.update(
|
109
|
+
%r{^(lib|app)/.*?([^/]+?)\\.rb$} => lambda do |matches|
|
110
|
+
name = matches[2]
|
111
|
+
"{test,spec}/**/#{name}_#{name.reverse}.rb"
|
112
|
+
end
|
113
|
+
)
|
114
|
+
.fi
|
115
|
+
.RE
|
116
|
+
.PP
|
117
|
+
\s+2\fBMulti\-glob expansion\fP\s-2
|
118
|
+
.PP
|
119
|
+
For example, if test files could optionally have "test" or "spec"
|
120
|
+
prefixed or appended to their already peculiar names, like so:
|
121
|
+
.RS
|
122
|
+
.IP \(bu 2
|
123
|
+
lib/hello.rb => test/hello_olleh_test.rb
|
124
|
+
.IP \(bu 2
|
125
|
+
lib/hello.rb => test/test_hello_olleh.rb
|
126
|
+
.IP \(bu 2
|
127
|
+
app/world.rb => spec/world_ldrow_spec.rb
|
128
|
+
.IP \(bu 2
|
129
|
+
app/world.rb => spec/spec_world_ldrow.rb
|
130
|
+
.RE
|
131
|
+
.PP
|
132
|
+
Then you would add the following to your configuration file:
|
133
|
+
.PP
|
134
|
+
.RS
|
135
|
+
.nf
|
136
|
+
Tork::Driver::TEST_FILE_GLOBBERS.update(
|
137
|
+
%r{^(lib|app)/.*?([^/]+?)\\.rb$} => lambda do |matches|
|
138
|
+
name = matches[2]
|
139
|
+
["{test,spec}/**/#{name}_#{name.reverse}.rb",
|
140
|
+
"{test,spec}/**/#{name}_#{name.reverse}_{test,spec}.rb",
|
141
|
+
"{test,spec}/**/{test,spec}_#{name}_#{name.reverse}.rb"]
|
142
|
+
end
|
143
|
+
)
|
144
|
+
.fi
|
145
|
+
.RE
|
146
|
+
.PP
|
147
|
+
\s+2\fBRecursive expansion\fP\s-2
|
148
|
+
.PP
|
149
|
+
For example, if you wanted to run test files associated with
|
150
|
+
\fB\fClib/hello.rb\fR whenever the \fB\fCapp/world.rb\fR file changed, then you would
|
151
|
+
write:
|
152
|
+
.PP
|
153
|
+
.RS
|
154
|
+
.nf
|
155
|
+
Tork::Driver::TEST_FILE_GLOBBERS.update(
|
156
|
+
%r{^app/world\\.rb$} => lambda do |matches|
|
157
|
+
'lib/hello.rb'
|
158
|
+
end
|
159
|
+
)
|
160
|
+
.fi
|
161
|
+
.RE
|
162
|
+
.PP
|
163
|
+
This effectively aliases one file onto another, but not in both
|
164
|
+
directions.
|
165
|
+
.PP
|
166
|
+
\s+2\fBSuppressing expansion\fP\s-2
|
167
|
+
.PP
|
168
|
+
These lambda functions can return \fB\fCnil\fR if they do not wish for a
|
169
|
+
particular source file to be tested. For example, to ignore tests for
|
170
|
+
all source files except those within a \fB\fCmodels/\fR directory, you would
|
171
|
+
write:
|
172
|
+
.PP
|
173
|
+
.RS
|
174
|
+
.nf
|
175
|
+
Tork::Driver::TEST_FILE_GLOBBERS.update(
|
176
|
+
%r{^(lib|app)(/.*?)([^/]+?)\\.rb$} => lambda do |matches|
|
177
|
+
if matches[2].include? '/models/'
|
178
|
+
["{test,spec}/**/#{matches[3]}_{test,spec}.rb",
|
179
|
+
"{test,spec}/**/{test,spec}_#{matches[3]}.rb"]
|
180
|
+
#else # implied by the Ruby language
|
181
|
+
#nil # implied by the Ruby language
|
182
|
+
end
|
183
|
+
end
|
184
|
+
)
|
185
|
+
.fi
|
186
|
+
.RE
|
187
|
+
.RE
|
188
|
+
.RE
|
189
|
+
.SH ENVIRONMENT
|
190
|
+
.PP
|
191
|
+
See
|
192
|
+
.BR tork (1).
|
65
193
|
.SH SEE ALSO
|
66
194
|
.PP
|
67
195
|
.BR tork (1),
|
196
|
+
.BR tork-remote (1),
|
68
197
|
.BR tork-herald (1),
|
69
|
-
.BR tork-driver (1),
|
70
198
|
.BR tork-engine (1),
|
71
199
|
.BR tork-master (1)
|