ZenTest 3.2.0 → 3.3.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 (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