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
@@ -0,0 +1,29 @@
1
+ require 'coverage'
2
+ at_exit do
3
+ if not $! or ($!.kind_of? SystemExit and $!.success?) and
4
+ coverage_by_file = Coverage.result rescue nil
5
+ then
6
+ report = {}
7
+ coverage_by_file.each do |file, coverage|
8
+ # ignore files outside working directory
9
+ if file.start_with? Dir.pwd
10
+ nsloc = 0
11
+ holes = []
12
+ coverage.each_with_index do |hits, index|
13
+ # ignore non-source lines of code
14
+ unless hits.nil?
15
+ nsloc += 1
16
+ # +1 because line numbers go 1..N
17
+ holes << index + 1 if hits.zero?
18
+ end
19
+ end
20
+
21
+ grade = ((nsloc - holes.length) / nsloc.to_f) * 100
22
+ report[file] = { grade: grade, nsloc: nsloc, holes: holes }
23
+ end
24
+ end
25
+
26
+ require 'yaml'
27
+ YAML.dump report, STDOUT
28
+ end
29
+ end
@@ -0,0 +1 @@
1
+ Coverage.start
@@ -0,0 +1,11 @@
1
+ Tork::Driver::ALL_TEST_FILE_GLOBS.push 'features/**/*.feature'
2
+
3
+ Tork::Driver::TEST_FILE_GLOBBERS.update(
4
+ # source files that correspond to test files
5
+ %r{^(features/(.+/)?)step_definitions/.+\.rb$} => lambda do |matches|
6
+ matches[1] + '*.feature'
7
+ end,
8
+
9
+ # the actual test files themselves
10
+ %r{^features/.+\.feature$} => lambda {|matches| matches[0] }
11
+ )
@@ -0,0 +1,14 @@
1
+ if $tork_test_file.end_with? '.feature'
2
+ original_argv = ARGV.dup
3
+ begin
4
+ # pass the feature file to cucumber(1) in ARGV
5
+ ARGV.push [$tork_test_file, *$tork_line_numbers].join(':')
6
+ require 'rubygems'
7
+ require 'cucumber'
8
+ load Gem.bin_path('cucumber', 'cucumber')
9
+ ensure
10
+ # restore ARGV for other at_exit hooks. otherwise, RSpec's hook will
11
+ # try to load the non-Ruby feature file from ARGV and fail accordingly.
12
+ ARGV.replace original_argv
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ ENV['TORK_CONFIGS'] += ':rails' if Dir['script/{rails,console}'].any?
2
+ ENV['TORK_CONFIGS'] += ':test' if File.directory? 'test'
3
+ ENV['TORK_CONFIGS'] += ':spec' if File.directory? 'spec'
4
+ ENV['TORK_CONFIGS'] += ':cucumber' if File.directory? 'features'
5
+ ENV['TORK_CONFIGS'] += ':factory_girl' if Dir['{test,spec}/factories/'].any?
@@ -0,0 +1,2 @@
1
+ dirname, basename = File.split($tork_log_file)
2
+ $tork_log_file.replace File.join(dirname, '.' + basename)
@@ -0,0 +1,2 @@
1
+ require 'factory_girl'
2
+ FactoryGirl.factories.clear
@@ -0,0 +1 @@
1
+ FactoryGirl.find_definitions
@@ -0,0 +1,4 @@
1
+ require 'fileutils'
2
+ dirname, basename = File.split($tork_log_file)
3
+ FileUtils.mkdir_p log_dir = File.join('log', dirname)
4
+ $tork_log_file.replace File.join(log_dir, basename)
@@ -0,0 +1,4 @@
1
+ # parallel_tests expects the sequence "", "2", "3", "4", ...
2
+ if $tork_worker_number > 0
3
+ ENV['TEST_ENV_NUMBER'] = ($tork_worker_number + 1).to_s
4
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/inflector'
2
+
3
+ Tork::Driver::REABSORB_FILE_GREPS.push(
4
+ %r{^config/.+\.(rb|yml)$},
5
+ %r{^db/schema\.rb$},
6
+ %r{^Gemfile\.lock$}
7
+ )
8
+
9
+ Tork::Driver::TEST_FILE_GLOBBERS.update(
10
+ %r{^(app|lib|test|spec)/.*?([^/]+?)(_factory)?\.rb$} => lambda do |matches|
11
+ single = matches[2]
12
+ plural = ActiveSupport::Inflector.pluralize(single)
13
+ "{test,spec}/**/{#{single},#{plural}_*}_{test,spec}.rb"
14
+ end
15
+ )
@@ -0,0 +1,12 @@
1
+ begin
2
+ require 'rails/railtie'
3
+ class Tork::Railtie < Rails::Railtie
4
+ config.before_initialize do |app|
5
+ app.config.cache_classes = false
6
+ end
7
+ end
8
+ rescue LoadError => error
9
+ warn "tork/config/rails/master: could not set configuration using railties;\n"\
10
+ "you will have to add the following to your test environment manually:\n\t"\
11
+ 'config.cache_classes = false'
12
+ end
@@ -0,0 +1,21 @@
1
+ # although inherited as a file descriptor during the fork(), the database
2
+ # connection cannot be shared between the master and its workers or amongst
3
+ # the workers themselves because access to the file descriptor isn't mutexed
4
+ if defined? ActiveRecord::Base
5
+ base = ActiveRecord::Base
6
+
7
+ info =
8
+ if base.respond_to? :connection_info # rails >= 3.1.0
9
+ base.connection_info
10
+ elsif base.respond_to? :connection_pool # rails >= 2.2.1
11
+ base.connection_pool.spec.config
12
+ else
13
+ warn "#{$0}: config/rails/worker: couldn't read connection information"
14
+ {}
15
+ end
16
+
17
+ # in-memory databases are an exception; they are simply fork()ed along
18
+ if info[:database] != ':memory:'
19
+ base.connection.reconnect!
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ Tork::Driver::REABSORB_FILE_GREPS.push 'spec/spec_helper.rb'
2
+
3
+ Tork::Driver::ALL_TEST_FILE_GLOBS.push 'spec/**/{spec_*,*_spec}.rb'
4
+
5
+ Tork::Driver::TEST_FILE_GLOBBERS.update(
6
+ # source files that correspond to test files
7
+ %r{^lib/.*?([^/]+)\.rb$} => lambda do |matches|
8
+ target = matches[1]
9
+ "spec/**/{spec_#{target},#{target}_spec}.rb"
10
+ end,
11
+
12
+ # the actual test files themselves
13
+ %r{^spec/.+\.rb$} => lambda do |matches|
14
+ target = matches[0]
15
+ target if File.basename(target) =~ /^spec_|_spec\./
16
+ end
17
+ )
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift 'spec' unless $LOAD_PATH.include? 'spec'
2
+ $LOAD_PATH.unshift 'lib' unless $LOAD_PATH.include? 'lib'
3
+ require 'spec_helper' if File.exist? 'spec/spec_helper.rb'
@@ -0,0 +1,3 @@
1
+ $tork_line_numbers.each do |line|
2
+ ARGV.push '--line_number', line.to_s
3
+ end
@@ -0,0 +1,17 @@
1
+ Tork::Driver::REABSORB_FILE_GREPS.push 'test/test_helper.rb'
2
+
3
+ Tork::Driver::ALL_TEST_FILE_GLOBS.push 'test/**/{test_*,*_test}.rb'
4
+
5
+ Tork::Driver::TEST_FILE_GLOBBERS.update(
6
+ # source files that correspond to test files
7
+ %r{^lib/.*?([^/]+)\.rb$} => lambda do |matches|
8
+ target = matches[1]
9
+ "test/**/{test_#{target},#{target}_test}.rb"
10
+ end,
11
+
12
+ # the actual test files themselves
13
+ %r{^test/.+\.rb$} => lambda do |matches|
14
+ target = matches[0]
15
+ target if File.basename(target) =~ /^test_|_test\./
16
+ end
17
+ )
@@ -0,0 +1,3 @@
1
+ $LOAD_PATH.unshift 'test' unless $LOAD_PATH.include? 'test'
2
+ $LOAD_PATH.unshift 'lib' unless $LOAD_PATH.include? 'lib'
3
+ require 'test_helper' if File.exist? 'test/test_helper.rb'
@@ -0,0 +1,23 @@
1
+ unless $tork_line_numbers.empty?
2
+ test_file_lines = File.readlines($tork_test_file)
3
+ test_names = $tork_line_numbers.map do |line|
4
+ catch :found do
5
+ # search backwards from the desired line number to
6
+ # the first line in the file for test definitions
7
+ line.downto(0) do |i|
8
+ test_name =
9
+ case test_file_lines[i]
10
+ when /^\s*def\s+test_(\w+)/ then $1
11
+ when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/
12
+ # elide string interpolation and invalid method name characters
13
+ $3.gsub(/\#\{.*?\}/, ' ').strip.gsub(/\W+/, '.*')
14
+ end \
15
+ and throw :found, test_name
16
+ end; nil # prevent unsuccessful search from returning an integer
17
+ end
18
+ end.compact.uniq
19
+
20
+ unless test_names.empty?
21
+ ARGV.push '--name', "/(?i:#{test_names.join('|')})/"
22
+ end
23
+ end
data/lib/tork/driver.rb CHANGED
@@ -1,59 +1,91 @@
1
1
  require 'set'
2
- require 'tork/client'
3
2
  require 'tork/engine'
3
+ require 'tork/server'
4
4
  require 'tork/config'
5
5
 
6
6
  module Tork
7
- class Driver < Engine
7
+ class Driver < Server
8
+
9
+ REABSORB_FILE_GREPS = []
10
+ ALL_TEST_FILE_GLOBS = []
11
+ TEST_FILE_GLOBBERS = {}
8
12
 
9
13
  def initialize
10
14
  super
11
-
12
- @herald = Client::Transceiver.new('tork-herald') do |changed_files|
13
- changed_files.each do |changed_file|
14
- # find and run the tests that correspond to the changed file
15
- visited = Set.new
16
- visitor = lambda do |source_file|
17
- Config.test_file_globbers.each do |regexp, globber|
18
- if regexp =~ source_file and globs = globber.call($~)
19
- Dir[*globs].each do |test_file|
20
- if visited.add? test_file
21
- run_test_file test_file
22
- visitor.call test_file
23
- end
24
- end
25
- end
26
- end
27
- end
28
- visitor.call changed_file
29
-
30
- # reabsorb text execution overhead if overhead files changed
31
- if Config.reabsorb_file_greps.any? {|r| r =~ changed_file }
32
- @client.send [:over, changed_file]
33
- reabsorb_overhead_files
34
- end
35
- end
36
- end
37
-
38
- reabsorb_overhead_files
15
+ Tork.config :driver
39
16
  end
40
17
 
41
- def quit
42
- @herald.quit
18
+ def loop
19
+ @herald = popen('tork-herald')
20
+ @engine = popen('tork-engine')
43
21
  super
22
+ ensure
23
+ pclose @herald
24
+ pclose @engine
44
25
  end
45
26
 
46
27
  def run_all_test_files
47
- all_test_files = Dir[*Config.all_test_file_globs]
28
+ all_test_files = Dir[*ALL_TEST_FILE_GLOBS]
48
29
  if all_test_files.empty?
49
- warn "#{$0}: There are no test files to run."
30
+ tell @client, 'There are no test files to run.'
50
31
  else
51
32
  run_test_files all_test_files
52
33
  end
53
34
  end
54
35
 
55
- def reabsorb_overhead_files
56
- reabsorb_overhead Config.overhead_load_paths, Dir[*Config.overhead_file_globs]
36
+ # accept and delegate tork-engine(1) commands
37
+ Engine.public_instance_methods(false).each do |name|
38
+ unless method_defined? name
39
+ define_method name do |*args|
40
+ send @engine, [name, *args]
41
+ end
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def recv client, message
48
+ case client
49
+ when @engine
50
+ send @clients, message # propagate downstream
51
+
52
+ when @herald
53
+ message.each do |changed_file|
54
+ # reabsorb text execution overhead if overhead files changed
55
+ overhead_changed = REABSORB_FILE_GREPS.any? do |pattern|
56
+ if pattern.kind_of? Regexp
57
+ pattern =~ changed_file
58
+ else
59
+ pattern == changed_file
60
+ end
61
+ end
62
+
63
+ if overhead_changed
64
+ send @clients, [:reabsorb, changed_file]
65
+ reabsorb_overhead
66
+ else
67
+ run_test_files find_dependent_test_files(changed_file).to_a
68
+ end
69
+ end
70
+
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def find_dependent_test_files source_file, results=Set.new
79
+ TEST_FILE_GLOBBERS.each do |regexp, globber|
80
+ if regexp =~ source_file and globs = globber.call($~)
81
+ Dir[*globs].each do |dependent_file|
82
+ if results.add? dependent_file
83
+ find_dependent_test_files dependent_file, results
84
+ end
85
+ end
86
+ end
87
+ end
88
+ results
57
89
  end
58
90
 
59
91
  end
data/lib/tork/engine.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'set'
2
2
  require 'diff/lcs'
3
- require 'tork/client'
4
3
  require 'tork/server'
5
4
  require 'tork/config'
6
5
 
@@ -9,49 +8,59 @@ class Engine < Server
9
8
 
10
9
  def initialize
11
10
  super
12
- @waiting_test_files = Set.new # dispatched to master but not yet running
13
- @running_test_files = Set.new # dispatched to master and started running
11
+ Tork.config :engine
12
+
13
+ @queued_test_files = Set.new
14
14
  @passed_test_files = Set.new
15
15
  @failed_test_files = Set.new
16
16
  @lines_by_file = {}
17
- @master = create_master_process
18
17
  end
19
18
 
20
- def quit
21
- @master.quit
19
+ def loop
20
+ create_master
22
21
  super
22
+ ensure
23
+ destroy_master
23
24
  end
24
25
 
25
- def reabsorb_overhead load_paths, overhead_files
26
- @master.quit
27
- @master = create_master_process
28
- @master.send [:load, load_paths, overhead_files]
26
+ def reabsorb_overhead
27
+ destroy_master
28
+ create_master
29
29
 
30
30
  # re-dispatch the previously dispatched files to the new master
31
- dispatched_test_files = @running_test_files + @waiting_test_files
32
- @waiting_test_files.clear
33
- run_test_files dispatched_test_files
31
+ previous = @queued_test_files.to_a
32
+ @queued_test_files.clear
33
+ run_test_files previous
34
34
  end
35
35
 
36
- def run_test_file test_file, line_numbers=nil
37
- if File.exist? test_file and @waiting_test_files.add? test_file
38
- line_numbers ||= find_changed_line_numbers(test_file)
39
- @master.send [:test, test_file, line_numbers]
36
+ def run_test_file test_file, *line_numbers
37
+ if File.exist? test_file and @queued_test_files.add? test_file
38
+ if line_numbers.empty?
39
+ line_numbers = find_changed_line_numbers(test_file)
40
+ else
41
+ line_numbers.map!(&:to_i)
42
+ line_numbers.clear if line_numbers.any?(&:zero?)
43
+ end
44
+ send @master, [:test, test_file, line_numbers]
40
45
  end
41
46
  end
42
47
 
43
- def stop_running_test_files
44
- if @running_test_files.empty?
45
- warn "#{$0}: There are no running test files to stop."
48
+ def run_test_files test_files_with_optional_line_numbers
49
+ test_files_with_optional_line_numbers.each {|f| run_test_file(*f) }
50
+ end
51
+
52
+ def stop_running_test_files signal=nil
53
+ if @queued_test_files.empty?
54
+ tell @client, 'There are no running test files to stop.'
46
55
  else
47
- @master.send [:stop]
48
- @running_test_files.clear
56
+ send @master, [:stop, signal].compact
57
+ @queued_test_files.clear
49
58
  end
50
59
  end
51
60
 
52
61
  def rerun_passed_test_files
53
62
  if @passed_test_files.empty?
54
- warn "#{$0}: There are no passed test files to re-run."
63
+ tell @client, 'There are no passed test files to re-run.'
55
64
  else
56
65
  run_test_files @passed_test_files
57
66
  end
@@ -59,7 +68,7 @@ class Engine < Server
59
68
 
60
69
  def rerun_failed_test_files
61
70
  if @failed_test_files.empty?
62
- warn "#{$0}: There are no failed test files to re-run."
71
+ tell @client, 'There are no failed test files to re-run.'
63
72
  else
64
73
  run_test_files @failed_test_files
65
74
  end
@@ -67,41 +76,37 @@ class Engine < Server
67
76
 
68
77
  protected
69
78
 
70
- def run_test_files files
71
- files.each {|f| run_test_file f }
72
- end
73
-
74
- private
75
-
76
- def create_master_process
77
- Client::Transceiver.new('tork-master') do |message|
78
- @client.send message # propagate output downstream
79
+ def recv client, message
80
+ case client
81
+ when @master
82
+ send @clients, message # propagate downstream
79
83
 
80
84
  event, file, line_numbers = message
81
85
  case event.to_sym
82
86
  when :test
83
- @waiting_test_files.delete file
84
- @running_test_files.add file
87
+ @queued_test_files.delete file
85
88
 
86
89
  when :pass
87
- @running_test_files.delete file
88
-
89
- # only whole test file runs qualify as pass
90
+ # only whole test file runs should qualify as pass
90
91
  if line_numbers.empty?
91
- @failed_test_files.delete file
92
- @passed_test_files.add file
92
+ was_fail = @failed_test_files.delete? file
93
+ now_pass = @passed_test_files.add? file
94
+ send @clients, [:fail_now_pass, file, message] if was_fail and now_pass
93
95
  end
94
96
 
95
97
  when :fail
96
- @running_test_files.delete file
97
- @failed_test_files.add file
98
- @passed_test_files.delete file
98
+ was_pass = @passed_test_files.delete? file
99
+ now_fail = @failed_test_files.add? file
100
+ send @clients, [:pass_now_fail, file, message] if was_pass and now_fail
99
101
  end
100
102
 
101
- Config.test_event_hooks.each {|hook| hook.call message }
103
+ else
104
+ super
102
105
  end
103
106
  end
104
107
 
108
+ private
109
+
105
110
  def find_changed_line_numbers test_file
106
111
  # cache test file contents for diffing below
107
112
  new_lines = File.readlines(test_file)
@@ -114,5 +119,13 @@ private
114
119
  map {|change| change.position + 1 }.uniq
115
120
  end
116
121
 
122
+ def create_master
123
+ @master = popen('tork-master')
124
+ end
125
+
126
+ def destroy_master
127
+ pclose @master
128
+ end
129
+
117
130
  end
118
131
  end