ZenTest 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/History.txt +21 -0
  2. data/Manifest.txt +8 -24
  3. data/Rakefile +32 -8
  4. data/bin/autotest +4 -1
  5. data/bin/multiruby +20 -6
  6. data/bin/ruby_fork +6 -0
  7. data/bin/ruby_fork_client +6 -0
  8. data/example_dot_autotest.rb +148 -0
  9. data/lib/autotest.rb +201 -254
  10. data/lib/rails_autotest.rb +48 -110
  11. data/lib/ruby_fork.rb +178 -0
  12. data/lib/test/rails.rb +2 -2
  13. data/lib/test/rails/controller_test_case.rb +1 -1
  14. data/lib/test/rails/helper_test_case.rb +60 -0
  15. data/lib/test/rails/pp_html_document.rb +74 -0
  16. data/lib/test/rails/rake_tasks.rb +13 -12
  17. data/lib/test/rails/view_test_case.rb +2 -2
  18. data/lib/test/zentest_assertions.rb +39 -0
  19. data/lib/unit_diff.rb +6 -3
  20. data/lib/zentest.rb +1 -1
  21. data/test/test_autotest.rb +160 -208
  22. data/test/test_rails_autotest.rb +115 -138
  23. data/test/test_ruby_fork.rb +172 -0
  24. data/test/test_unit_diff.rb +69 -1
  25. data/test/test_zentest_assertions.rb +66 -0
  26. metadata +13 -27
  27. data/test/data/normal/lib/.#photo.rb +0 -0
  28. data/test/data/normal/lib/blah.rb +0 -0
  29. data/test/data/normal/lib/photo.rb +0 -0
  30. data/test/data/normal/test/#test_photo.rb# +0 -0
  31. data/test/data/normal/test/test_camelcase.rb +0 -0
  32. data/test/data/normal/test/test_photo.rb +0 -0
  33. data/test/data/normal/test/test_route.rb +0 -0
  34. data/test/data/normal/test/test_user.rb +0 -0
  35. data/test/data/rails/app/controllers/admin/theme_controller.rb +0 -0
  36. data/test/data/rails/app/controllers/route_controller.rb +0 -0
  37. data/test/data/rails/app/models/flickr_photo.rb +0 -0
  38. data/test/data/rails/app/models/route.rb +0 -0
  39. data/test/data/rails/app/views/route/index.rhtml +0 -0
  40. data/test/data/rails/config/environment.rb +0 -0
  41. data/test/data/rails/config/routes.rb +0 -0
  42. data/test/data/rails/test/controllers/route_controller_test.rb +0 -0
  43. data/test/data/rails/test/fixtures/routes.yml +0 -0
  44. data/test/data/rails/test/functional/admin/themes_controller_test.rb +0 -0
  45. data/test/data/rails/test/functional/dummy_controller_test.rb +0 -0
  46. data/test/data/rails/test/functional/route_controller_test.rb +0 -0
  47. data/test/data/rails/test/unit/flickr_photo_test.rb +0 -0
  48. data/test/data/rails/test/unit/photo_test.rb +0 -0
  49. data/test/data/rails/test/unit/route_test.rb +0 -0
  50. data/test/data/rails/test/views/route_view_test.rb +0 -0
data/History.txt CHANGED
@@ -1,3 +1,24 @@
1
+ *** 3.3.0 / 2006-07-28
2
+
3
+ + 1 major enhancement:
4
+ + autotest has been rewritten to be much cleaner, now has a plugin system.
5
+ + 5 minor enhancement:
6
+ + test/rails adds helper tests, "stolen" from Geoff's work, (which was "stolen" from ryan's work. :P)
7
+ + autotest turnaround is now faster.
8
+ + Added more prune dirs to autotest.
9
+ + test/rails rewinds IOs containing captured output. Added assert_empty.
10
+ + Document that autotest doesn't run the db:test:prepare rake task when in Rails mode.
11
+ + Added ruby_fork, but haven't fully plugged into autotest yet.
12
+ + 7 bug fixes:
13
+ + Add SIGINT handler to unit_diff to give a more graceful exit.
14
+ + Don't strip <> from tempfiles, parse_diff does it for us.
15
+ + Fixed autotest problems on windoze. Ugh.
16
+ + Fixed broken pipe bug and newline bug in unit_diff.
17
+ + Make request_method in ControllerTestCase a String.
18
+ + multitest installs rubygems if tarball found in versions dir.
19
+ + multitest only configures when makefile is missing. Rebuilds much faster now.
20
+ + ruby_fork exits without backtrace and allows redirection of output.
21
+
1
22
  *** 3.2.0 / 2006-04-10
2
23
 
3
24
  + 1 major enhancement:
data/Manifest.txt CHANGED
@@ -6,48 +6,32 @@ Rakefile
6
6
  bin/autotest
7
7
  bin/multiruby
8
8
  bin/rails_test_audit
9
+ bin/ruby_fork
10
+ bin/ruby_fork_client
9
11
  bin/unit_diff
10
12
  bin/zentest
11
13
  example.txt
12
14
  example1.rb
13
15
  example2.rb
16
+ example_dot_autotest.rb
14
17
  lib/autotest.rb
15
18
  lib/rails_autotest.rb
19
+ lib/ruby_fork.rb
16
20
  lib/test/rails.rb
17
21
  lib/test/rails/controller_test_case.rb
18
22
  lib/test/rails/functional_test_case.rb
23
+ lib/test/rails/helper_test_case.rb
19
24
  lib/test/rails/ivar_proxy.rb
25
+ lib/test/rails/pp_html_document.rb
20
26
  lib/test/rails/rake_tasks.rb
21
27
  lib/test/rails/test_case.rb
22
28
  lib/test/rails/view_test_case.rb
23
29
  lib/test/zentest_assertions.rb
24
30
  lib/unit_diff.rb
25
31
  lib/zentest.rb
26
- test/data/normal/lib/.#photo.rb
27
- test/data/normal/lib/blah.rb
28
- test/data/normal/lib/photo.rb
29
- test/data/normal/test/#test_photo.rb#
30
- test/data/normal/test/test_camelcase.rb
31
- test/data/normal/test/test_photo.rb
32
- test/data/normal/test/test_route.rb
33
- test/data/normal/test/test_user.rb
34
- test/data/rails/app/controllers/admin/theme_controller.rb
35
- test/data/rails/app/controllers/route_controller.rb
36
- test/data/rails/app/models/flickr_photo.rb
37
- test/data/rails/app/models/route.rb
38
- test/data/rails/app/views/route/index.rhtml
39
- test/data/rails/config/environment.rb
40
- test/data/rails/config/routes.rb
41
- test/data/rails/test/controllers/route_controller_test.rb
42
- test/data/rails/test/fixtures/routes.yml
43
- test/data/rails/test/functional/admin/themes_controller_test.rb
44
- test/data/rails/test/functional/dummy_controller_test.rb
45
- test/data/rails/test/functional/route_controller_test.rb
46
- test/data/rails/test/unit/flickr_photo_test.rb
47
- test/data/rails/test/unit/photo_test.rb
48
- test/data/rails/test/unit/route_test.rb
49
- test/data/rails/test/views/route_view_test.rb
50
32
  test/test_autotest.rb
51
33
  test/test_rails_autotest.rb
34
+ test/test_ruby_fork.rb
52
35
  test/test_unit_diff.rb
53
36
  test/test_zentest.rb
37
+ test/test_zentest_assertions.rb
data/Rakefile CHANGED
@@ -4,6 +4,7 @@ require 'rake'
4
4
  require 'rake/testtask'
5
5
  require 'rake/rdoctask'
6
6
  require 'rake/gempackagetask'
7
+ require 'rake/contrib/sshpublisher'
7
8
  require 'rbconfig'
8
9
 
9
10
  require './lib/zentest.rb'
@@ -25,8 +26,12 @@ spec = Gem::Specification.new do |s|
25
26
  s.instance_variable_set "@description", paragraphs[3..10].join("\n\n")
26
27
  s.instance_variable_set "@summary", paragraphs[12]
27
28
 
29
+ s.homepage = "http://www.zenspider.com/ZSS/Products/ZenTest/"
30
+ s.rubyforge_project = "zentest"
31
+ s.has_rdoc = true
32
+
28
33
  if $DEBUG then
29
- puts "ZenTest #{s.version}"
34
+ puts "#{s.name} #{s.version}"
30
35
  puts
31
36
  puts s.executables.sort.inspect
32
37
  puts
@@ -36,10 +41,6 @@ spec = Gem::Specification.new do |s|
36
41
  puts "** description:"
37
42
  puts s.description
38
43
  end
39
-
40
- s.homepage = "http://www.zenspider.com/ZSS/Products/ZenTest/"
41
- s.rubyforge_project = "zentest"
42
- s.has_rdoc = true
43
44
  end
44
45
 
45
46
  desc 'Build Gem'
@@ -69,6 +70,16 @@ Rake::RDocTask.new :rdoc do |rd|
69
70
  rd.options << '-d' if `which dot` =~ /\/dot/ unless RUBY_PLATFORM =~ /win32/
70
71
  rd.options << '-t ZenTest RDoc'
71
72
  end
73
+
74
+ desc 'Upload RDoc to RubyForge'
75
+ task :upload => :rdoc do
76
+
77
+ user = "#{ENV['USER']}@rubyforge.org"
78
+ project = '/var/www/gforge-projects/zentest'
79
+ local_dir = 'doc'
80
+ pub = Rake::SshDirPublisher.new user, project, local_dir
81
+ pub.upload
82
+ end
72
83
 
73
84
  $prefix = ENV['PREFIX'] || Config::CONFIG['prefix']
74
85
  $bin = File.join($prefix, 'bin')
@@ -76,9 +87,8 @@ $lib = Config::CONFIG['sitelibdir']
76
87
  $bins = spec.executables
77
88
  $libs = spec.files.grep(/^lib\//).map { |f| f.sub(/^lib\//, '') }.sort
78
89
 
79
- task :blah do
80
- p $bins
81
- p $libs
90
+ task :autotest do
91
+ ruby "-Ilib ./bin/autotest"
82
92
  end
83
93
 
84
94
  task :install do
@@ -114,5 +124,19 @@ task :clean => [ :clobber_rdoc, :clobber_package ] do
114
124
  rm_f Dir["**/*~"]
115
125
  end
116
126
 
127
+ task :help do
128
+ Rake.application.options.show_task_pattern = //
129
+ Rake.application.display_tasks_and_comments
130
+ end
131
+
132
+ task :sort do
133
+ begin
134
+ sh 'for f in lib/*.rb; do echo $f; grep "^ *def " $f | grep -v sort=skip > x; sort x > y; echo $f; echo; diff x y; done'
135
+ sh 'for f in test/test_*.rb; do echo $f; grep "^ *def.test_" $f > x; sort x > y; echo $f; echo; diff x y; done'
136
+ ensure
137
+ sh 'rm x y'
138
+ end
139
+ end
140
+
117
141
  # vim:syntax=ruby
118
142
 
data/bin/autotest CHANGED
@@ -17,7 +17,10 @@ if $h or $help then
17
17
  help << nil
18
18
  help << "\t-rails\t\tForce rails mode."
19
19
  help << "\t\t\tRails will be automatically detected by the presence of"
20
- help << "\t\t\tconfig/environment.rb"
20
+ help << "\t\t\tconfig/environment.rb. Use this if you don't have one."
21
+ help << nil
22
+ help << "\t\t\tYou may need to run 'rake db:test:prepare' before"
23
+ help << "\t\t\tstarting autotest on a rails project."
21
24
  help << nil
22
25
  help << "\t-vcs=NAME\tVersion control system to update."
23
26
  help << "\t\t\tAutotest will automatically update every vcstime"
data/bin/multiruby CHANGED
@@ -36,8 +36,13 @@ Dir.chdir root_dir do
36
36
  end
37
37
  end
38
38
 
39
+ tarballs = Dir["versions/rubygems*.tgz"]
40
+ raise "You should delete all but one rubygem tarball" if tarballs.size > 1
41
+ rubygem_tarball = File.expand_path tarballs.last
42
+
39
43
  Dir.chdir "build" do
40
44
  Dir["../versions/ruby*.tar.gz"].each do |tarball|
45
+ next if tarball =~ /rubygems/
41
46
  build_dir = File.basename tarball, ".tar.gz"
42
47
  version = build_dir.sub(/^ruby-?/, '')
43
48
  versions << version
@@ -46,15 +51,26 @@ Dir.chdir root_dir do
46
51
  unless test ?d, build_dir then
47
52
  puts "creating #{inst_dir}"
48
53
  Dir.mkdir inst_dir
49
- puts "unpacking #{tarball}"
50
- system "tar zxf #{tarball}"
54
+ run "tar zxf #{tarball}"
51
55
  end
52
56
  Dir.chdir build_dir do
57
+ puts "building and installing #{version}"
53
58
  run "autoconf" unless test ?f, "configure"
54
59
  FileUtils.rm_r "ext/readline" if test ?d, "ext/readline"
55
- run "./configure --prefix #{inst_dir} &> log.configure"
56
- run "make -j4 &> log.build"
60
+ run "./configure --prefix #{inst_dir} &> log.configure" unless test ?f, "Makefile"
61
+ run "nice make -j4 &> log.build"
57
62
  run "make install &> log.install"
63
+ build_dir = Dir.pwd
64
+
65
+ if rubygem_tarball then
66
+ next if version.gsub(/\./, '') =~ /^19/ # doesn't seem to work w/ ruby 1.9, filed bug
67
+ rubygems = File.basename rubygem_tarball, ".tgz"
68
+ run "tar zxf #{rubygem_tarball}" unless test ?d, rubygems
69
+
70
+ Dir.chdir rubygems do
71
+ run "../ruby ./setup.rb &> ../log.rubygems"
72
+ end
73
+ end
58
74
  end
59
75
  end
60
76
  end
@@ -78,5 +94,3 @@ puts
78
94
  puts "TOTAL RESULT = #{result} failures"
79
95
 
80
96
  exit result
81
-
82
-
data/bin/ruby_fork ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'ruby_fork'
4
+
5
+ RubyFork.start_server ARGV
6
+
@@ -0,0 +1,6 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'ruby_fork'
4
+
5
+ RubyFork.start_client ARGV
6
+
@@ -0,0 +1,148 @@
1
+ # -*- ruby -*-
2
+
3
+ module AutoGrowl
4
+ def self.growl title, msg, pri=0
5
+ system "growlnotify -n autotest --image /Applications/Mail.app/Contents/Resources/Caution.tiff -p #{pri} -m #{msg.inspect} #{title}"
6
+ end
7
+
8
+ Autotest.add_hook :run do |at|
9
+ growl "Run", "Run" unless $TESTING
10
+ end
11
+
12
+ Autotest.add_hook :red do |at|
13
+ growl "Tests Failed", "#{at.files_to_test.size} tests failed", 2
14
+ end
15
+
16
+ Autotest.add_hook :green do |at|
17
+ growl "Tests Passed", "All tests passed", -2 if at.tainted
18
+ end
19
+
20
+ Autotest.add_hook :init do |at|
21
+ growl "autotest", "autotest was started" unless $TESTING
22
+ end
23
+
24
+ Autotest.add_hook :interrupt do |at|
25
+ growl "autotest", "autotest was reset" unless $TESTING
26
+ end
27
+
28
+ Autotest.add_hook :quit do |at|
29
+ growl "autotest", "autotest is exiting" unless $TESTING
30
+ end
31
+
32
+ Autotest.add_hook :all do |at|_hook
33
+ growl "autotest", "Tests have fully passed", -2 unless $TESTING
34
+ end
35
+ end
36
+
37
+ module HtmlConsole
38
+ MAX = 30
39
+ STATUS = {}
40
+ PATH = File.expand_path("~/Sites/autotest.html")
41
+
42
+ def self.update
43
+ STATUS.delete STATUS.keys.sort.last if STATUS.size > MAX
44
+ File.open(PATH, "w") do |f|
45
+ f.puts "<title>Autotest Status</title>"
46
+ STATUS.sort.reverse.each do |t,s|
47
+ if s > 0 then
48
+ f.puts "<p style=\"color:red\">#{t}: #{s}"
49
+ else
50
+ f.puts "<p style=\"color:green\">#{t}: #{s}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ Autotest.add_hook :red do |at|
57
+ STATUS[Time.now] = at.files_to_test.size
58
+ update
59
+ end
60
+
61
+ Autotest.add_hook :green do |at|
62
+ STATUS[Time.now] = 0
63
+ update
64
+ end
65
+ end
66
+
67
+ require 'osx/cocoa'
68
+ include Math
69
+ include OSX
70
+
71
+ OSX::NSBundle.bundleWithPath(File.expand_path("~/Library/Frameworks/Aquaterm.framework")).load
72
+ OSX.ns_import :AQTAdapter
73
+
74
+ class StatusBoard
75
+ BLACK = 0
76
+ WHITE = 1
77
+ RED = 2
78
+ GREEN = 3
79
+ GRAY = 4
80
+
81
+ def initialize
82
+ @past = []
83
+
84
+ @adapter = AQTAdapter.alloc.init
85
+ @adapter.openPlotWithIndex 1
86
+ @adapter.setPlotSize([122,122])
87
+ @adapter.setPlotTitle("Autotest Status")
88
+
89
+ @adapter.setColormapEntry_red_green_blue(0, 0.0, 0.0, 0.0) # black
90
+ @adapter.setColormapEntry_red_green_blue(1, 1.0, 1.0, 1.0) # white
91
+ @adapter.setColormapEntry_red_green_blue(2, 1.0, 0.0, 0.0) # red
92
+ @adapter.setColormapEntry_red_green_blue(3, 0.0, 1.0, 0.0) # green
93
+ @adapter.setColormapEntry_red_green_blue(4, 0.7, 0.7, 0.7) # gray
94
+
95
+ draw
96
+ end
97
+
98
+ def draw
99
+ # @past = @past[10..-1] if @past.size >= 100
100
+ @past.shift if @past.size > 100
101
+
102
+ @adapter.takeColorFromColormapEntry(@past.last ? GREEN : RED)
103
+ @adapter.addFilledRect([0, 0, 122, 122])
104
+
105
+ @adapter.takeColorFromColormapEntry(BLACK)
106
+ @adapter.addFilledRect([10, 10, 102, 102])
107
+
108
+ @adapter.takeColorFromColormapEntry(GRAY)
109
+ @adapter.addFilledRect([11, 11, 100, 100])
110
+
111
+ @adapter.takeColorFromColormapEntry(0)
112
+
113
+ @past.each_with_index do |passed,i|
114
+ x = i % 10
115
+ y = i / 10
116
+
117
+ @adapter.takeColorFromColormapEntry(passed ? GREEN : RED)
118
+ @adapter.addFilledRect([x*10+11, y*10+11, 10, 10])
119
+ end
120
+ @adapter.renderPlot
121
+ end
122
+
123
+ def pass
124
+ @past.push true
125
+ draw
126
+ end
127
+
128
+ def fail
129
+ @past.push false
130
+ draw
131
+ end
132
+
133
+ def close
134
+ @adapter.closePlot
135
+ end
136
+ end
137
+
138
+ unless $TESTING then
139
+ board = StatusBoard.new
140
+
141
+ Autotest.add_hook :red do |at|
142
+ board.fail unless $TESTING
143
+ end
144
+
145
+ Autotest.add_hook :green do |at|
146
+ board.pass unless $TESTING
147
+ end
148
+ end
data/lib/autotest.rb CHANGED
@@ -1,342 +1,289 @@
1
- $TESTING = defined? $TESTING
2
-
3
1
  require 'find'
4
2
  require 'rbconfig'
5
3
 
4
+ $TESTING = false unless defined? $TESTING
5
+
6
6
  ##
7
- # Autotest continuously runs your tests as you work on your project.
7
+ # Autotest continuously scans the files in your project for changes
8
+ # and runs the appropriate tests. Test failures are run until they
9
+ # have all passed. Then the full test suite is run to ensure that
10
+ # nothing else was inadvertantly broken.
11
+ #
12
+ # If you want Autotest to start over from the top, hit ^C once. If
13
+ # you want Autotest to quit, hit ^C twice.
14
+ #
15
+ # Rails:
16
+ #
17
+ # The autotest command will automatically discover a Rails directory
18
+ # by looking for config/environment.rb. When Rails is discovered,
19
+ # autotest uses RailsAutotest to perform file mappings and other work.
20
+ # See RailsAutotest for details.
8
21
  #
9
- # Autotest periodically scans the files in your project for updates then
10
- # figures out the appropriate tests to run and runs them. If a test fails
11
- # Autotest will run just that test until you get it to pass.
22
+ # Plugins:
12
23
  #
13
- # If you want Autotest to start over from the top, hit ^C. If you want
14
- # Autotest to quit, hit ^C twice.
24
+ # Plugins are available by creating a .autotest file either in your
25
+ # project root or in your home directory. You can then write event
26
+ # handlers in the form of:
15
27
  #
16
- # Autotest uses a simple naming scheme to figure out how to map implementation
17
- # files to test files following the Test::Unit naming scheme.
28
+ # Autotest.add_hook hook_name { |autotest| ... }
29
+ #
30
+ # The available hooks are: run, interrupt, quit, ran_command, red,
31
+ # green, all_good, and reset.
32
+ #
33
+ # See example_dot_autotest.rb for more details.
34
+ #
35
+ # Naming:
36
+ #
37
+ # Autotest uses a simple naming scheme to figure out how to map
38
+ # implementation files to test files following the Test::Unit naming
39
+ # scheme.
18
40
  #
19
41
  # * Test files must be stored in test/
20
42
  # * Test files names must start with test_
21
- # * Test classes must start with Test
43
+ # * Test class names must start with Test
22
44
  # * Implementation files must be stored in lib/
23
45
  # * Implementation files must match up with a test file named
24
46
  # test_.*implementation.rb
25
- #--
26
- # New (proposed) strategy:
47
+ #
48
+ # Strategy:
27
49
  #
28
50
  # 1) find all files and associate them from impl <-> test
29
51
  # 2) run all tests
30
52
  # 3) scan for failures
31
53
  # 4) detect changes in ANY (ruby?) file, rerun all failures + changed files
32
- # NOTE: this runs in a loop, loop handling should be improved slightly to
33
- # have less crap (ruby command, failure count).
34
54
  # 5) until 0 defects, goto 3
35
55
  # 6) when 0 defects, goto 2
36
56
 
37
57
  class Autotest
38
-
58
+
59
+ HOOKS = Hash.new { |h,k| h[k] = [] }
60
+
39
61
  def self.run
40
62
  new.run
41
63
  end
42
64
 
43
- ##
44
- # Creates a new Autotest. If @exceptions is set, updated_files will use it
45
- # to reject filenames.
65
+ attr_accessor :exceptions, :files, :files_to_test, :interrupted, :last_mtime, :libs, :output, :tainted
46
66
 
47
67
  def initialize
48
- @interrupt = false
49
68
  @files = Hash.new Time.at(0)
50
- @exceptions = nil
69
+ @files_to_test = Hash.new { |h,k| h[k] = [] }
70
+ @exceptions = false
71
+ @libs = '.:lib:test'
72
+ @output = $stderr
73
+ @sleep = 2
51
74
  end
52
75
 
53
- ##
54
- # Consolidates failed tests +failed+ for the same test class into a single
55
- # test runner filter. Also maps failed class to its file regexp.
56
-
57
- def consolidate_failures(failed)
58
- filters = Hash.new { |h,k| h[k] = [] }
59
-
60
- failed.each do |method, klass|
61
- failed_file = klass.sub('Test', '').gsub(/(.)([A-Z])/, '\1_?\2')
62
- filters[failed_file.downcase] << method
76
+ def run
77
+ hook :run
78
+ reset
79
+ add_sigint_handler
80
+
81
+ loop do # ^c handler
82
+ begin
83
+ get_to_green
84
+ rerun_all_tests if @tainted
85
+ wait_for_changes
86
+ rescue Interrupt
87
+ if @wants_to_quit then
88
+ break
89
+ else
90
+ reset
91
+ hook :interrupt
92
+ end
93
+ end
63
94
  end
95
+ hook :quit
96
+ end
64
97
 
65
- return filters.map do |klass, methods|
66
- ["'/^(#{methods.join('|')})$/'", /#{klass}/]
98
+ def get_to_green
99
+ until all_good do
100
+ run_tests
101
+ wait_for_changes unless all_good
67
102
  end
68
103
  end
69
104
 
105
+ def run_tests
106
+ find_files_to_test # failed + changed/affected
107
+ cmd = make_test_cmd @files_to_test
70
108
 
71
- ##
72
- # Selects test files to run that match failures in +failed_file+ based on
73
- # +updated_files+ and +tests+.
74
- #
75
- # Only test files matching +failed_file+ will be returned so the test
76
- # runner's -n flag will correctly match the failed tests.
77
- #--
78
- # failed_test_files must never check for updated files, retest_failed reuses
79
- # +updated_files+.
80
-
81
- def failed_test_files(failed_file, tests, updated_files)
82
- return [] if updated_files.empty?
83
-
84
- updated_tests = updated_files.select { |f| f =~ /^test/ }
109
+ puts cmd
85
110
 
86
- tests_to_filter = if updated_files == updated_tests then
87
- updated_tests
88
- else
89
- files = (updated_files + tests).uniq
90
- tests_to_filter = map_file_names(files).flatten.uniq
91
- end
111
+ @results = `#{cmd}`
112
+ hook :ran_command
113
+ puts @results
92
114
 
93
- return tests_to_filter.select { |test| test =~ failed_file }
115
+ handle_results(@results)
94
116
  end
95
117
 
96
- ##
97
- # Returns a report of remaining failures in +failures+.
118
+ ############################################################
119
+ # Utility Methods, not essential to reading of logic
98
120
 
99
- def failure_report(failed)
100
- out = []
101
- out << "# failures remain in #{failed.length} files:"
102
- failed.each do |filter, failed_test|
103
- tests = @files.keys.select { |file| file =~ /^test.*#{failed_test}/ }
104
- test = tests.sort_by { |f| f.length }.first
105
-
106
- filter =~ /\((.*)\)/
107
- filter = $1.split('|')
108
-
109
- out << "# #{test}:"
110
- out << "# #{filter.join "\n# "}"
121
+ def add_sigint_handler
122
+ trap 'INT' do
123
+ if @interrupted then
124
+ @wants_to_quit = true
125
+ else
126
+ puts "Interrupt a second time to quit"
127
+ @interrupted = true
128
+ sleep 1.5
129
+ raise Interrupt # let the run loop catch it
130
+ end
111
131
  end
112
-
113
- return out.join("\n")
114
132
  end
115
133
 
116
- ##
117
- # Maps implementation files to test files. Returns an Array of one or more
118
- # Arrays of test filenames.
134
+ def all_good
135
+ @files_to_test.empty?
136
+ end
119
137
 
120
- def map_file_names(updated)
121
- tests = []
138
+ def consolidate_failures(failed)
139
+ filters = Hash.new { |h,k| h[k] = [] }
122
140
 
123
- updated.each do |filename|
124
- case filename
125
- when %r%^lib/(?:.*/)?(.*\.rb)$% then
126
- impl = $1.gsub '_', '_?'
127
- found = @files.keys.select do |k|
128
- k =~ %r%^test/.*#{impl}$%
129
- end
130
- tests.push(*found)
131
- when %r%^test/test_% then
132
- tests << filename # always run tests
133
- when %r%^(doc|pkg)/% then
134
- # ignore
141
+ failed.each do |method, klass|
142
+ failed_file_name = klass.gsub(/(.)([A-Z])/, '\1_?\2')
143
+ failed_files = @files.keys.grep(/#{failed_file_name}/i)
144
+ case failed_files.size
145
+ when 0 then
146
+ @output.puts "Unable to map class #{klass} to a file" # FIX for testing
147
+ when 1 then
148
+ filters[failed_files.last] << method
135
149
  else
136
- STDERR.puts "Dunno! #{filename}" if $v or $TESTING
150
+ @output.puts "multiple files matched class #{klass} #{failed_files.inspect}."
151
+ # nothing yet
137
152
  end
138
153
  end
139
154
 
140
- return [tests]
155
+ return filters
141
156
  end
142
157
 
143
- ##
144
- # Resets all file timestamps in the list
158
+ def find_files
159
+ result = {}
160
+ Find.find '.' do |f|
161
+ Find.prune if @exceptions and f =~ @exceptions and test ?d, f
162
+ Find.prune if f =~ /(\.(svn|hg)|CVS|tmp|public|doc|pkg)$/ # prune dirs
145
163
 
146
- def reset_times
147
- ago = Time.at 0
148
- @files.each_key { |file| @files[file] = ago }
149
- end
164
+ next if test ?d, f
165
+ next if f =~ /(swp|~|rej|orig)$/ # temporary/patch files
166
+ next if f =~ /\/\.?#/ # Emacs autosave/cvs merge files
150
167
 
151
- ##
152
- # Retests failed tests.
153
- #--
154
- # Runs through each failure and runs tests matching the failure. If an
155
- # implementation file was updated all failed tests should be run.
156
- #
157
- # TODO collapse multiple failures in the same file (in test) and use | in
158
- # the filter.
159
-
160
- def retest_failed(failed, tests)
161
- puts "# Waiting for changes"
162
-
163
- # -t and -n includes all tests that match either filter, not tests that
164
- # match both filters, so figure out which TestCase to run from the filename,
165
- # and use -n on that.
166
- until failed.empty? do
167
- sleep 5 unless $TESTING
168
-
169
- updated = updated_files
170
-
171
- # REFACTOR
172
- failed.map! do |filter, failed_test|
173
- failed_files = failed_test_files failed_test, tests, updated
174
- break [filter, failed_test] if failed_files.empty?
175
-
176
- puts "# Rerunning failures: #{failed_files.join ' '}"
177
-
178
- test_filter = " -n #{filter}" unless filter == "'/^(default_test)/'"
179
- cmd = "#{ruby} -Ilib:test #{failed_files.join ' '}#{test_filter} | unit_diff -u"
180
-
181
- puts "+ #{cmd}"
182
- result = `#{cmd}`
183
- puts result
184
- status = result.split($/).last
185
- rerun = status =~ / 0 failures, 0 errors/ ? nil : [filter, failed_test]
186
- puts "# Waiting for changes" if rerun
187
- rerun # needed for map!
188
- end
168
+ filename = f.sub(/^\.\//, '')
189
169
 
190
- puts failure_report(failed) if failed.compact! and not failed.empty?
170
+ result[filename] = File.stat(filename).mtime
191
171
  end
172
+ return result
192
173
  end
193
174
 
194
- ##
195
- # The path to this ruby for running tests.
196
-
197
- def ruby
198
- return File.join(Config::CONFIG['bindir'],
199
- Config::CONFIG['ruby_install_name'])
200
- end
175
+ def find_files_to_test(files=find_files)
176
+ updated = []
201
177
 
202
- ##
203
- # Repeatedly scans for updated files and runs their tests.
178
+ # TODO: keep an mtime at app level and drop the files hash
179
+ files.each do |filename, mtime|
180
+ next if @files[filename] >= mtime
204
181
 
205
- def run
206
- trap 'INT' do
207
- if @interrupt then
208
- puts "# Ok, you really want to quit, doing so"
209
- exit
182
+ tests_for_file(filename).each do |f|
183
+ @files_to_test[f] # creates key with default value
210
184
  end
211
- # STDERR.puts "\t#{caller.join "\n\t"}"
212
- puts "# hit ^C again to quit"
213
- sleep 1.5 # give them enough time to hit ^C again
214
- @interrupt = true # if they hit ^C again,
215
- raise Interrupt # let the run loop catch it
216
- end
217
-
218
- begin
219
- last_update = Time.at 0
220
-
221
- loop do
222
- if $vcs and Time.now > last_update + $vcstime then
223
- last_update = Time.now
224
- case $vcs
225
- when 'cvs' then
226
- system 'cvs up'
227
- when 'p4' then
228
- system 'p4 sync'
229
- when 'svn' then
230
- system 'svn up'
231
- else
232
- puts "# Sorry, I don't know what version control system \"#{$vcs}\" is"
233
- end
234
- end
235
185
 
236
- files = updated_files
237
- test files unless files.empty?
238
- sleep 5
239
- end
240
- rescue Interrupt
241
- @interrupt = false # they didn't hit ^C in time
242
- puts "# ok, restarting from the top"
243
- @files.clear
244
- retry
186
+ @files[filename] = mtime
245
187
  end
246
- end
247
-
248
- ##
249
- # Runs tests for files in +updated+. Implementation files are looked up
250
- # with map_file_names.
251
- #
252
- # Returns true if any of the tests ever failed.
253
-
254
- def test(updated)
255
- ever_failed = false
256
-
257
- map_file_names(updated).each do |tests|
258
- next if tests.empty?
259
- puts '# Testing updated files'
260
- cmd = "#{ruby} -Ilib:test -e '#{tests.inspect}.each { |f| load f }' | unit_diff -u"
261
- puts "+ #{cmd}"
262
- results = `#{cmd}`
263
- puts results
264
-
265
- if results =~ / 0 failures, 0 errors\Z/ then
266
- puts '# Passed'
267
- next
268
- end
269
188
 
270
- ever_failed = true
271
-
272
- failed = results.scan(/^\s+\d+\) (?:Failure|Error):\n(.*?)\((.*?)\)/)
273
-
274
- if failed.empty? then
275
- puts '# Test::Unit exited without a parseable failure or error message.'
276
- puts '# You probably have a syntax error in your code.'
277
- puts '# I\'ll retry in 10 seconds'
278
- sleep 10
279
- redo
280
- end
189
+ previous = @last_mtime
190
+ @last_mtime = @files.values.sort.last
191
+ @last_mtime > previous
192
+ end
281
193
 
282
- failed = consolidate_failures(failed)
194
+ def handle_results(results)
195
+ failed = results.scan(/^\s+\d+\) (?:Failure|Error):\n(.*?)\((.*?)\)/)
196
+ @files_to_test = consolidate_failures failed
197
+ unless @files_to_test.empty? then
198
+ hook :red
199
+ else
200
+ hook :green
201
+ end unless $TESTING
202
+ @tainted = true unless @files_to_test.empty?
203
+ end
283
204
 
284
- # REFACTOR: I don't think the two routines merit real differences
285
- retest_failed failed, tests
205
+ def make_test_cmd files_to_test
206
+ cmds = []
207
+ full, partial = files_to_test.partition { |k,v| v.empty? }
286
208
 
287
- break
209
+ unless full.empty? then
210
+ classes = full.map {|k,v| k}.flatten.join(' ')
211
+ cmds << "#{ruby} -I#{@libs} -rtest/unit -e \"%w[#{classes}].each { |f| load f }\" | unit_diff -u"
288
212
  end
289
213
 
290
- if ever_failed
291
- reset_times
292
- else # We'll immediately test everything, so don't print this out.
293
- puts '# All passed'
294
- puts "# Waiting for changes"
214
+ partial.each do |klass, methods|
215
+ cmds << "#{ruby} -I#{@libs} #{klass} -n \"/^(#{Regexp.union(*methods).source})$/\" | unit_diff -u"
295
216
  end
296
217
 
297
- return ever_failed
218
+ return cmds.join('; ')
298
219
  end
299
220
 
300
- ##
301
- # Returns true or false if the file has been modified or not. New files are
302
- # always modified.
221
+ def rerun_all_tests
222
+ reset
223
+ run_tests
224
+ hook :all_good if all_good
225
+ end
303
226
 
304
- def updated?(file)
305
- mtime = File.stat(file).mtime
306
- updated = @files[file] < mtime
307
- @files[file] = mtime
308
- return updated
227
+ def reset
228
+ @interrupted = @wants_to_quit = false
229
+ @files.clear
230
+ @files_to_test.clear
231
+ @last_mtime = Time.at(0)
232
+ find_files_to_test # failed + changed/affected
233
+ @tainted = false
234
+ hook :reset
309
235
  end
236
+
237
+ def ruby
238
+ ruby = File.join(Config::CONFIG['bindir'],
239
+ Config::CONFIG['ruby_install_name'])
310
240
 
311
- ##
312
- # Returns names of files that have been modified since updated_files was
313
- # last run. Files and paths can be ignored by setting @exceptions in
314
- # initialize.
241
+ unless File::ALT_SEPARATOR.nil? then
242
+ ruby.gsub! File::SEPARATOR, File::ALT_SEPARATOR
243
+ end
315
244
 
316
- def updated_files
317
- updated = []
245
+ return ruby
246
+ end
318
247
 
319
- Find.find '.' do |f|
320
- if @exceptions then
321
- if f =~ @exceptions then
322
- Find.prune if Kernel.test ?d, f
323
- next
324
- end
248
+ def tests_for_file(filename)
249
+ case filename
250
+ when /^lib\/.*\.rb$/ then
251
+ impl = File.basename(filename).gsub '_', '_?'
252
+ @files.keys.select do |k|
253
+ k =~ %r%^test/.*#{impl}$%
325
254
  end
255
+ when /^test\/test_.*rb$/ then
256
+ [filename]
257
+ else
258
+ @output.puts "Dunno! #{filename}" if $TESTING
259
+ []
260
+ end
261
+ end
326
262
 
327
- Find.prune if f =~ /(?:\.svn|CVS)$/ # version control files
328
-
329
- next if File.directory? f
330
- next if f =~ /(?:swp|~|rej|orig)$/ # temporary/patch files
331
- next if f =~ /\/\.?#/ # Emacs autosave/cvs merge files
263
+ def wait_for_changes
264
+ begin
265
+ sleep @sleep
266
+ end until find_files_to_test
267
+ end
332
268
 
333
- f = f.sub(/^\.\//, '')
269
+ ############################################################
270
+ # Hooks:
334
271
 
335
- updated << f if updated? f
272
+ def hook(name)
273
+ HOOKS[name].each do |plugin|
274
+ plugin[self]
336
275
  end
337
-
338
- return updated
339
276
  end
340
277
 
278
+ def self.add_hook(name, &block)
279
+ HOOKS[name] << block
280
+ end
341
281
  end
342
282
 
283
+ if test ?f, './.autotest' then
284
+ load './.autotest'
285
+ elsif test ?f, File.expand_path('~/.autotest') then
286
+ load File.expand_path('~/.autotest')
287
+ else
288
+ puts "couldn't find ./.autotest in #{Dir.pwd}"
289
+ end