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.
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)