tork 18.2.4 → 19.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/HISTORY.markdown +65 -0
  2. data/README.markdown +60 -298
  3. data/bin/tork +99 -62
  4. data/bin/tork-driver +124 -25
  5. data/bin/tork-engine +45 -23
  6. data/bin/tork-herald +5 -5
  7. data/bin/tork-master +79 -32
  8. data/bin/tork-notify +70 -0
  9. data/bin/tork-remote +80 -0
  10. data/lib/tork/cliapp.rb +73 -0
  11. data/lib/tork/config.rb +14 -97
  12. data/lib/tork/config/coverage/master.rb +29 -0
  13. data/lib/tork/config/coverage/worker.rb +1 -0
  14. data/lib/tork/config/cucumber/driver.rb +11 -0
  15. data/lib/tork/config/cucumber/worker.rb +14 -0
  16. data/lib/tork/config/default/config.rb +5 -0
  17. data/lib/tork/config/dotlog/onfork.rb +2 -0
  18. data/lib/tork/config/factory_girl/onfork.rb +2 -0
  19. data/lib/tork/config/factory_girl/worker.rb +1 -0
  20. data/lib/tork/config/logdir/onfork.rb +4 -0
  21. data/lib/tork/config/parallel_tests/worker.rb +4 -0
  22. data/lib/tork/config/rails/driver.rb +15 -0
  23. data/lib/tork/config/rails/master.rb +12 -0
  24. data/lib/tork/config/rails/worker.rb +21 -0
  25. data/lib/tork/config/spec/driver.rb +17 -0
  26. data/lib/tork/config/spec/master.rb +3 -0
  27. data/lib/tork/config/spec/worker.rb +3 -0
  28. data/lib/tork/config/test/driver.rb +17 -0
  29. data/lib/tork/config/test/master.rb +3 -0
  30. data/lib/tork/config/test/worker.rb +23 -0
  31. data/lib/tork/driver.rb +68 -36
  32. data/lib/tork/engine.rb +57 -44
  33. data/lib/tork/master.rb +34 -34
  34. data/lib/tork/server.rb +118 -17
  35. data/lib/tork/version.rb +1 -1
  36. data/man/man1/tork-driver.1 +165 -37
  37. data/man/man1/tork-engine.1 +50 -32
  38. data/man/man1/tork-herald.1 +5 -8
  39. data/man/man1/tork-master.1 +95 -42
  40. data/man/man1/tork-notify.1 +27 -0
  41. data/man/man1/tork-remote.1 +38 -0
  42. data/man/man1/tork.1 +116 -8
  43. data/tork.gemspec +3 -3
  44. metadata +36 -25
  45. data/lib/tork/client.rb +0 -53
  46. data/lib/tork/config/coverage.rb +0 -40
  47. data/lib/tork/config/cucumber.rb +0 -32
  48. data/lib/tork/config/dotlog.rb +0 -8
  49. data/lib/tork/config/factory_girl.rb +0 -10
  50. data/lib/tork/config/logdir.rb +0 -10
  51. data/lib/tork/config/notify.rb +0 -34
  52. data/lib/tork/config/parallel_tests.rb +0 -9
  53. 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 ... Config.max_forked_workers).to_a
22
+ @worker_number_pool = (0 ... MAX_CONCURRENT_WORKERS).to_a
11
23
  @command_by_worker_pid = {}
12
24
  end
13
25
 
14
- def load paths, files
15
- $LOAD_PATH.unshift(*paths)
16
-
17
- @overhead_files = files.each do |file|
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 < Config.max_forked_workers
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
- Config.before_fork_hooks.each do |hook|
36
- hook.call test_file, line_numbers, log_file, worker_number
37
- end
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
- Config.after_fork_hooks.each do |hook|
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.push(log_file, worker_number)
65
- @client.send @command
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
- worker_status = Process.wait2(worker_pid).last
70
- command = @command_by_worker_pid.delete(worker_pid)
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] = if worker_status.success? then :pass else :fail end
73
- @client.send command.push(worker_status.to_i, worker_status.inspect)
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 :SIGTERM, *@command_by_worker_pid.keys.map {|pid| -pid }
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 'tork/client'
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 initialize
7
- trap(:SIGTERM){ quit }
9
+ def self.address program=$0
10
+ ".#{program}.sock"
8
11
  end
9
12
 
10
- def quit
11
- Thread.exit # kill Client::Receiver in loop()
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
- @client = Client::Transmitter.new(STDOUT.dup)
16
- STDOUT.reopen(STDERR).sync = true
17
-
18
- Client::Receiver.new(STDIN) do |command|
19
- if command.first != __method__ # prevent loops
20
- @command = command
21
- begin
22
- __send__(*command)
23
- rescue => error
24
- warn "#{$0}: #{error}"
25
- warn error.backtrace.join("\n")
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.join
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
@@ -1,3 +1,3 @@
1
1
  module Tork
2
- VERSION = "18.2.4"
2
+ VERSION = "19.0.0"
3
3
  end
@@ -1,4 +1,4 @@
1
- .TH TORK\-DRIVER 1 2012\-10\-10 18.2.4
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
- according to
15
- .BR tork-herald (1)'s
16
- observations.
17
- It reads the following single\-line commands (JSON arrays) from its standard
18
- input stream and performs the respective actions as described below. It also
19
- funnels the standard output stream of
20
- .BR tork-engine (1)
21
- into its own.
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
- This program accepts
30
+ Commands for
32
31
  .BR tork-engine (1)
33
- commands and delegates them accordingly.
32
+ are also accepted here.
33
+ .SS Output
34
34
  .PP
35
- When
36
- .BR tork-herald (1)
37
- reports that a file belonging to the test execution
38
- overhead has been modified, this program replaces
39
- .BR tork-master (1)
40
- with a new
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
- \fB\fC["over",\fR \fIoverhead_file\fP\fB\fC]\fR
47
- The test execution overhead is currently being reabsorbed, by replacing
42
+ \fI...\fP
43
+ Messages from
44
+ .BR tork-engine (1)
45
+ and
48
46
  .BR tork-master (1)
49
- with a new instance, because \fIoverhead_file\fP has changed.
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 for configuring
58
- .BR tork (1).
59
- .SH ENVIRONMENT
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\fCTORK_CONFIGS\fR
62
- A single\-line JSON array containing paths to actual files or names of
63
- helper libraries in the tork/config/ namespace of Ruby's load path.
64
- These configuration files are loaded just before \fI.tork.rb\fP is loaded.
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)