tork 15.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.
@@ -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
@@ -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
@@ -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
@@ -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,7 @@
1
+ require 'tork/config'
2
+
3
+ Tork::Config.after_fork_hooks << proc do |worker_number|
4
+ # for compatitibilty with parallel_tests gem,
5
+ # store numbers as strings: "", "2", "3", "4"
6
+ ENV['TEST_ENV_NUMBER'] = (worker_number + 1).to_s if worker_number > 0
7
+ 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
@@ -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
@@ -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