ryanbriones-ZenTest 3.11.1

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 (70) hide show
  1. data/History.txt +523 -0
  2. data/Manifest.txt +69 -0
  3. data/README.txt +110 -0
  4. data/Rakefile +68 -0
  5. data/articles/Article.css +721 -0
  6. data/articles/getting_started_with_autotest.html +532 -0
  7. data/articles/how_to_use_zentest.txt +393 -0
  8. data/bin/autotest +55 -0
  9. data/bin/multiruby +40 -0
  10. data/bin/multiruby_setup +68 -0
  11. data/bin/rails_test_audit +80 -0
  12. data/bin/unit_diff +38 -0
  13. data/bin/zentest +28 -0
  14. data/example.txt +42 -0
  15. data/example1.rb +7 -0
  16. data/example2.rb +15 -0
  17. data/example_dot_autotest.rb +45 -0
  18. data/lib/autotest.rb +654 -0
  19. data/lib/autotest/autoupdate.rb +26 -0
  20. data/lib/autotest/camping.rb +37 -0
  21. data/lib/autotest/cctray.rb +57 -0
  22. data/lib/autotest/discover.rb +6 -0
  23. data/lib/autotest/emacs.rb +35 -0
  24. data/lib/autotest/email_notify.rb +66 -0
  25. data/lib/autotest/fixtures.rb +12 -0
  26. data/lib/autotest/growl.rb +28 -0
  27. data/lib/autotest/heckle.rb +14 -0
  28. data/lib/autotest/html_report.rb +31 -0
  29. data/lib/autotest/jabber_notify.rb +111 -0
  30. data/lib/autotest/kdenotify.rb +14 -0
  31. data/lib/autotest/menu.rb +51 -0
  32. data/lib/autotest/migrate.rb +7 -0
  33. data/lib/autotest/notify.rb +34 -0
  34. data/lib/autotest/once.rb +9 -0
  35. data/lib/autotest/pretty.rb +83 -0
  36. data/lib/autotest/rails.rb +81 -0
  37. data/lib/autotest/rcov.rb +22 -0
  38. data/lib/autotest/redgreen.rb +21 -0
  39. data/lib/autotest/restart.rb +11 -0
  40. data/lib/autotest/screen.rb +73 -0
  41. data/lib/autotest/shame.rb +45 -0
  42. data/lib/autotest/snarl.rb +51 -0
  43. data/lib/autotest/timestamp.rb +9 -0
  44. data/lib/functional_test_matrix.rb +92 -0
  45. data/lib/multiruby.rb +401 -0
  46. data/lib/test/rails.rb +295 -0
  47. data/lib/test/rails/controller_test_case.rb +382 -0
  48. data/lib/test/rails/functional_test_case.rb +79 -0
  49. data/lib/test/rails/helper_test_case.rb +64 -0
  50. data/lib/test/rails/ivar_proxy.rb +31 -0
  51. data/lib/test/rails/pp_html_document.rb +74 -0
  52. data/lib/test/rails/rake_tasks.rb +50 -0
  53. data/lib/test/rails/render_tree.rb +93 -0
  54. data/lib/test/rails/test_case.rb +28 -0
  55. data/lib/test/rails/view_test_case.rb +597 -0
  56. data/lib/test/zentest_assertions.rb +134 -0
  57. data/lib/unit_diff.rb +259 -0
  58. data/lib/zentest.rb +566 -0
  59. data/lib/zentest_mapping.rb +99 -0
  60. data/test/test_autotest.rb +449 -0
  61. data/test/test_help.rb +36 -0
  62. data/test/test_rails_autotest.rb +229 -0
  63. data/test/test_rails_controller_test_case.rb +58 -0
  64. data/test/test_rails_helper_test_case.rb +48 -0
  65. data/test/test_rails_view_test_case.rb +275 -0
  66. data/test/test_unit_diff.rb +319 -0
  67. data/test/test_zentest.rb +566 -0
  68. data/test/test_zentest_assertions.rb +128 -0
  69. data/test/test_zentest_mapping.rb +222 -0
  70. metadata +151 -0
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'multiruby'
4
+
5
+ ARGV << "help" if ARGV.empty?
6
+
7
+ Dir.chdir Multiruby.root_dir
8
+ Multiruby.setup_dirs(false)
9
+
10
+ ARGV.each do |spec|
11
+ case spec
12
+ when "-h", "help" then
13
+ Multiruby.help
14
+ exit 0
15
+ when "list" then
16
+ Multiruby.list
17
+ exit 0
18
+ when "build" then
19
+ # do nothing
20
+ when /rm:(.*)/ then
21
+ Multiruby.rm $1
22
+ when "clean" then
23
+ Multiruby.clean
24
+ exit 0
25
+ when "update" then
26
+ Multiruby.update
27
+ when "update:rubygems" then
28
+ Multiruby.update_rubygems
29
+ when "rubygems:merge" then
30
+ Multiruby.merge_rubygems
31
+ when "mri:svn:current" then
32
+ ARGV << "mri:svn:releases" << "mri:svn:branches"
33
+ when "mri:svn:releases" then
34
+ Multiruby::TAGS.each do |v|
35
+ latest = Multiruby.mri_latest_tag v
36
+ abort "Can't find tag #{v}" unless latest
37
+ ARGV << "mri:svn:tag:#{latest}:mri_rel_#{v}"
38
+ end
39
+ when "mri:svn:branches" then
40
+ Multiruby::BRANCHES.each do |v|
41
+ ARGV << "mri:svn:branch:#{v}"
42
+ end
43
+ when /mri:svn:branch:(.*)/ then
44
+ dir = ver = $1
45
+ ver = "branches/ruby_#{ver}" unless ver == "trunk"
46
+
47
+ Multiruby.svn_co "#{Multiruby::MRI_SVN}/#{ver}", "mri_#{dir}"
48
+ when /mri:svn:tag:(.*):(.*)/ then
49
+ ver, dir = $1, $2
50
+
51
+ Multiruby.svn_co "#{Multiruby::MRI_SVN}/tags/#{ver}", dir
52
+ when /mri:svn:tag:(.*)/ then
53
+ ver = $1
54
+
55
+ ARGV << "mri:svn:tag:#{ver}:#{ver}"
56
+ when /mri:tar:(.*)/ then
57
+ v = $1
58
+ Multiruby.fetch_tar v
59
+ when /rbx:ln:(.*)/ then
60
+ Multiruby.rbx_ln $1
61
+ when /rbx:git:current/ then
62
+ Multiruby.git_clone "#{Multiruby::RBX_GIT}/code", "rubinius"
63
+ else
64
+ warn "unknown spec #{spec}"
65
+ end
66
+ end
67
+
68
+ Multiruby.build_and_install
@@ -0,0 +1,80 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # TODO: Probably has dup code with ZenTest.
4
+ # TODO: Map controller assert_assigns to view test ivar assignments.
5
+ # TODO: Make this a rake task, rake test:audit.
6
+
7
+ # test_cases[test_case][test][ivar] = value
8
+ def build_test_cases(type)
9
+ test_cases = Hash.new { |h,k|
10
+ h[k] = Hash.new { |h,k|
11
+ h[k] = Hash.new { |h,k|
12
+ h[k] = {} } } }
13
+
14
+ test_case = nil
15
+ test = nil
16
+
17
+ fixtures = Hash.new { |h,k| h[k] = [] }
18
+
19
+ Dir["test/#{type}/*rb"].each do |test|
20
+ File.open test do |fp|
21
+ fp.each_line do |line|
22
+ case line
23
+ when /^class (.*)(View|Controller)Test </
24
+ test_case = $1
25
+ when /^\s+def (test_\S+)/
26
+ test = $1
27
+ when /^\s+controller\[(.+?)\] = (.*)$/,
28
+ /^\s+assert_assigned (.*?), (.*)/
29
+ ivar = $1
30
+ value = $2
31
+ test_cases[test_case][test][ivar] = value
32
+ when /fixtures (.*)/ then
33
+ fixtures[test_case].push(*$1.split(', '))
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ return test_cases, fixtures
40
+ end
41
+
42
+ view_test_cases, view_fixtures = build_test_cases 'views'
43
+ controller_test_cases, controller_fixtures = build_test_cases 'controller'
44
+
45
+ out = []
46
+
47
+ view_test_cases.sort_by { |tc,_| tc }.each do |test_case, tests|
48
+ out << "require 'test/test_helper'"
49
+ out << nil
50
+ out << "class #{test_case}ControllerTest < Test::Rails::ControllerTestCase"
51
+ out << nil
52
+
53
+ fixtures = controller_fixtures[test_case] - view_fixtures[test_case]
54
+
55
+ unless fixtures.empty? then
56
+ fixtures.each do |fixture|
57
+ out << " fixtures #{fixture}"
58
+ end
59
+ out << nil
60
+ end
61
+
62
+ tests.sort_by { |t,_| t }.each do |test, ivars|
63
+ ivars = view_test_cases[test_case][test].keys -
64
+ controller_test_cases[test_case][test].keys
65
+
66
+ next if ivars.empty?
67
+
68
+ out << " def #{test}"
69
+ ivars.sort.each do |ivar|
70
+ value = view_test_cases[test_case][test][ivar]
71
+ out << " assert_assigned #{ivar}, #{value}"
72
+ end
73
+ out << " end"
74
+ out << nil
75
+ end
76
+ out << "end"
77
+ out << nil
78
+ end
79
+
80
+ puts out.join("\n")
data/bin/unit_diff ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/local/bin/ruby -ws
2
+ #
3
+ # unit_diff - a ruby unit test filter by Ryan Davis <ryand-ruby@zenspider.com>
4
+ #
5
+ # usage:
6
+ #
7
+ # test.rb | unit_diff [options]
8
+ # options:
9
+ # -b ignore whitespace differences
10
+ # -c contextual diff
11
+ # -h show usage
12
+ # -k keep temp diff files around
13
+ # -l prefix line numbers on the diffs
14
+ # -u unified diff
15
+ # -v display version
16
+
17
+ require 'unit_diff'
18
+ require 'zentest'
19
+
20
+ ############################################################
21
+
22
+ if defined? $v then
23
+ puts "#{File.basename $0} v. #{ZenTest::VERSION}"
24
+ exit 0
25
+ end
26
+
27
+ if defined? $h then
28
+ File.open($0) do |f|
29
+ begin; end until f.readline =~ /usage:/
30
+ f.readline
31
+ while line = f.readline and line.sub!(/^# ?/, '')
32
+ $stderr.puts line
33
+ end
34
+ end
35
+ exit 0
36
+ end
37
+
38
+ UnitDiff.unit_diff
data/bin/zentest ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/local/bin/ruby -swI .
2
+
3
+ require 'ZenTest'
4
+
5
+ $TESTING = true # for ZenWeb and any other testing infrastructure code
6
+
7
+ if defined? $v then
8
+ puts "#{File.basename $0} v#{ZenTest::VERSION}"
9
+ exit 0
10
+ end
11
+
12
+ if defined? $h then
13
+ puts "usage: #{File.basename $0} [-h -v] test-and-implementation-files..."
14
+ puts " -h display this information"
15
+ puts " -v display version information"
16
+ puts " -r Reverse mapping (ClassTest instead of TestClass)"
17
+ puts " -e (Rapid XP) eval the code generated instead of printing it"
18
+ exit 0
19
+ end
20
+
21
+ code = ZenTest.fix(*ARGV)
22
+ if defined? $e then
23
+ require 'test/unit'
24
+ eval code
25
+ else
26
+ print code
27
+ end
28
+
data/example.txt ADDED
@@ -0,0 +1,42 @@
1
+
2
+
3
+
4
+ What do we do to get people writing tests?
5
+ What do we do to get people writing tests first?
6
+
7
+ I didn't know it's name, but apparently it's the Lettuce Principal,
8
+ (see http://blog.toolshed.com/2003/03/the_lettuce_pri.html).
9
+
10
+ We NEED to make testing as easy as possible to get them testing.
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+ Class Under Test Test Class
27
+ ######################################################################
28
+
29
+ module Something module TestSomething
30
+ class Thingy class TestThingy
31
+ def do_something def test_do_something_normal
32
+ # ... thingy = Thingy.new
33
+ end result = thingy.do_something
34
+ end assert(result.blahblah)
35
+ end end
36
+ def test_do_something_edgecase
37
+ thingy = Thingy.new
38
+ result = thingy.do_something
39
+ assert(result.blahblah)
40
+ end
41
+ end
42
+ end
data/example1.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Something
2
+ class Thingy
3
+ def do_something
4
+ # ...
5
+ end
6
+ end
7
+ end
data/example2.rb ADDED
@@ -0,0 +1,15 @@
1
+ module TestSomething
2
+ class TestThingy
3
+ def test_do_something_normal
4
+ thingy = Thingy.new
5
+ result = thingy.do_something
6
+ assert(result.blahblah)
7
+ end
8
+ def test_do_something_edgecase
9
+ thingy = Thingy.new
10
+ result = thingy.do_something
11
+ assert(result.blahblah)
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,45 @@
1
+ # -*- ruby -*-
2
+
3
+ # require 'autotest/autoupdate'
4
+ # require 'autotest/camping'
5
+ # require 'autotest/cctray'
6
+ # require 'autotest/emacs'
7
+ # require 'autotest/email_notify'
8
+ # require 'autotest/fixtures'
9
+ # require 'autotest/growl'
10
+ # require 'autotest/heckle'
11
+ # require 'autotest/html_report'
12
+ # require 'autotest/jabber_notify'
13
+ # require 'autotest/kdenotify'
14
+ # require 'autotest/menu'
15
+ # require 'autotest/migrate'
16
+ # require 'autotest/notify'
17
+ # require 'autotest/once'
18
+ # require 'autotest/pretty'
19
+ # require 'autotest/rcov'
20
+ # require 'autotest/redgreen'
21
+ # require 'autotest/restart'
22
+ # require 'autotest/screen'
23
+ # require 'autotest/shame'
24
+ # require 'autotest/snarl'
25
+ # require 'autotest/timestamp'
26
+
27
+ # Autotest::AutoUpdate.sleep_time = o
28
+ # Autotest::AutoUpdate.update_cmd = o
29
+ # Autotest::CCTray.project_name = name
30
+ # Autotest::Emacs.command = o
31
+ # Autotest::EmailNotify.smtp_settings = o
32
+ # Autotest::EmailNotify.from = o
33
+ # Autotest::EmailNotify.recipients = o
34
+ # Autotest::EmailNotify.use_svn = o
35
+ # Autotest::EmailNotify.report_every_run = o
36
+ # Autotest::Growl.growl title, msg, pri = 0, img = nil
37
+ # Autotest::JabberNotify.recipients = o
38
+ # Autotest::JabberNotify.account = o
39
+ # Autotest::JabberNotify.password = o
40
+ # Autotest::JabberNotify.use_svn = o
41
+ # Autotest::JabberNotify.report_every_run = o
42
+ # Autotest::RCov.command = o
43
+ # Autotest::RCov.pattern = o
44
+ # Autotest::Shame.chat_app = o
45
+ # Autotest::Snarl.snarl title, msg, ico = nil
data/lib/autotest.rb ADDED
@@ -0,0 +1,654 @@
1
+ require 'find'
2
+ require 'rbconfig'
3
+
4
+ $v ||= false
5
+ $TESTING = false unless defined? $TESTING
6
+
7
+ ##
8
+ # Autotest continuously scans the files in your project for changes
9
+ # and runs the appropriate tests. Test failures are run until they
10
+ # have all passed. Then the full test suite is run to ensure that
11
+ # nothing else was inadvertantly broken.
12
+ #
13
+ # If you want Autotest to start over from the top, hit ^C once. If
14
+ # you want Autotest to quit, hit ^C twice.
15
+ #
16
+ # Rails:
17
+ #
18
+ # The autotest command will automatically discover a Rails directory
19
+ # by looking for config/environment.rb. When Rails is discovered,
20
+ # autotest uses RailsAutotest to perform file mappings and other work.
21
+ # See RailsAutotest for details.
22
+ #
23
+ # Plugins:
24
+ #
25
+ # Plugins are available by creating a .autotest file either in your
26
+ # project root or in your home directory. You can then write event
27
+ # handlers in the form of:
28
+ #
29
+ # Autotest.add_hook hook_name { |autotest| ... }
30
+ #
31
+ # The available hooks are: initialize, run, run_command, ran_command,
32
+ # red, green, updated, all_good, reset, interrupt, and quit.
33
+ #
34
+ # See example_dot_autotest.rb for more details.
35
+ #
36
+ # If a hook returns a true value, it signals to autotest that the hook
37
+ # was handled and should not continue executing hooks.
38
+ #
39
+ # Naming:
40
+ #
41
+ # Autotest uses a simple naming scheme to figure out how to map
42
+ # implementation files to test files following the Test::Unit naming
43
+ # scheme.
44
+ #
45
+ # * Test files must be stored in test/
46
+ # * Test files names must start with test_
47
+ # * Test class names must start with Test
48
+ # * Implementation files must be stored in lib/
49
+ # * Implementation files must match up with a test file named
50
+ # test_.*implementation.rb
51
+ #
52
+ # Strategy:
53
+ #
54
+ # 1. Find all files and associate them from impl <-> test.
55
+ # 2. Run all tests.
56
+ # 3. Scan for failures.
57
+ # 4. Detect changes in ANY (ruby?. file, rerun all failures + changed files.
58
+ # 5. Until 0 defects, goto 3.
59
+ # 6. When 0 defects, goto 2.
60
+
61
+ class Autotest
62
+
63
+ T0 = Time.at 0
64
+
65
+ ALL_HOOKS = [ :all_good, :initialize, :interrupt, :quit, :ran_command,
66
+ :reset, :run_command, :waiting ]
67
+
68
+ HOOKS = Hash.new { |h,k| h[k] = [] }
69
+ unless defined? WINDOZE then
70
+ WINDOZE = /win32/ =~ RUBY_PLATFORM
71
+ SEP = WINDOZE ? '&' : ';'
72
+ end
73
+
74
+ @@discoveries = []
75
+
76
+ ##
77
+ # Add a proc to the collection of discovery procs. See
78
+ # +autodiscover+.
79
+
80
+ def self.add_discovery &proc
81
+ @@discoveries << proc
82
+ end
83
+
84
+ ##
85
+ # Automatically find all potential autotest runner styles by
86
+ # searching your loadpath, vendor/plugins, and rubygems for
87
+ # "autotest/discover.rb". If found, that file is loaded and it
88
+ # should register discovery procs with autotest using
89
+ # +add_discovery+. That proc should return one or more strings
90
+ # describing the user's current environment. Those styles are then
91
+ # combined to dynamically invoke an autotest plugin to suite your
92
+ # environment. That plugin should define a subclass of Autotest with
93
+ # a corresponding name.
94
+ #
95
+ # === Process:
96
+ #
97
+ # 1. All autotest/discover.rb files loaded.
98
+ # 2. Those procs determine your styles (eg ["rails", "rspec"]).
99
+ # 3. Require file by sorting styles and joining (eg 'autotest/rails_rspec').
100
+ # 4. Invoke run method on appropriate class (eg Autotest::RailsRspec.run).
101
+ #
102
+ # === Example autotest/discover.rb:
103
+ #
104
+ # Autotest.add_discovery do
105
+ # "rails" if File.exist? 'config/environment.rb'
106
+ # end
107
+ #
108
+
109
+ def self.autodiscover
110
+ style = []
111
+
112
+ paths = $:.dup
113
+ paths.push 'lib'
114
+ paths.push(*Dir["vendor/plugins/*/lib"])
115
+
116
+ begin
117
+ require 'rubygems'
118
+ paths.push(*Gem.latest_load_paths)
119
+ rescue LoadError => e
120
+ # do nothing
121
+ end
122
+
123
+ paths.each do |d|
124
+ next unless File.directory? d
125
+ f = File.join(d, 'autotest', 'discover.rb')
126
+ if File.exist? f then
127
+ $: << d unless $:.include? d
128
+ load f
129
+ end
130
+ end
131
+
132
+ @@discoveries.map { |proc| proc.call }.flatten.compact.sort.uniq
133
+ end
134
+
135
+ ##
136
+ # Initialize and run the system.
137
+
138
+ def self.run
139
+ new.run
140
+ end
141
+
142
+ attr_writer :known_files
143
+ attr_accessor(:completed_re,
144
+ :extra_class_map,
145
+ :extra_files,
146
+ :failed_results_re,
147
+ :files_to_test,
148
+ :find_order,
149
+ :interrupted,
150
+ :last_mtime,
151
+ :libs,
152
+ :order,
153
+ :output,
154
+ :results,
155
+ :sleep,
156
+ :tainted,
157
+ :find_directories,
158
+ :unit_diff,
159
+ :wants_to_quit)
160
+
161
+ ##
162
+ # Initialize the instance and then load the user's .autotest file, if any.
163
+
164
+ def initialize
165
+ # these two are set directly because they're wrapped with
166
+ # add/remove/clear accessor methods
167
+ @exception_list = []
168
+ @test_mappings = []
169
+
170
+ self.completed_re = /\d+ tests, \d+ assertions, \d+ failures, \d+ errors/
171
+ self.extra_class_map = {}
172
+ self.extra_files = []
173
+ self.failed_results_re = /^\s+\d+\) (?:Failure|Error):\n(.*?)\((.*?)\)/
174
+ self.files_to_test = new_hash_of_arrays
175
+ self.find_order = []
176
+ self.known_files = nil
177
+ self.libs = %w[. lib test].join(File::PATH_SEPARATOR)
178
+ self.order = :random
179
+ self.output = $stderr
180
+ self.sleep = 1
181
+ self.find_directories = ['.']
182
+ self.unit_diff = "unit_diff -u"
183
+
184
+ self.add_mapping(/^lib\/.*\.rb$/) do |filename, _|
185
+ possible = File.basename(filename).gsub '_', '_?'
186
+ files_matching %r%^test/.*#{possible}$%
187
+ end
188
+
189
+ self.add_mapping(/^test.*\/test_.*rb$/) do |filename, _|
190
+ filename
191
+ end
192
+
193
+ [File.expand_path('~/.autotest'), './.autotest'].each do |f|
194
+ load f if File.exist? f
195
+ end
196
+ end
197
+
198
+ ##
199
+ # Repeatedly run failed tests, then all tests, then wait for changes
200
+ # and carry on until killed.
201
+
202
+ def run
203
+ hook :initialize
204
+ reset
205
+ add_sigint_handler
206
+
207
+ self.last_mtime = Time.now if $f
208
+
209
+ loop do # ^c handler
210
+ begin
211
+ get_to_green
212
+ if self.tainted then
213
+ rerun_all_tests
214
+ else
215
+ hook :all_good
216
+ end
217
+ wait_for_changes
218
+ rescue Interrupt
219
+ break if self.wants_to_quit
220
+ reset
221
+ end
222
+ end
223
+ hook :quit
224
+ end
225
+
226
+ ##
227
+ # Keep running the tests after a change, until all pass.
228
+
229
+ def get_to_green
230
+ begin
231
+ run_tests
232
+ wait_for_changes unless all_good
233
+ end until all_good
234
+ end
235
+
236
+ ##
237
+ # Look for files to test then run the tests and handle the results.
238
+
239
+ def run_tests
240
+ hook :run_command
241
+
242
+ new_mtime = self.find_files_to_test
243
+ return unless new_mtime
244
+ self.last_mtime = new_mtime
245
+
246
+ cmd = self.make_test_cmd self.files_to_test
247
+ return if cmd.empty?
248
+
249
+ puts cmd unless $q
250
+
251
+ old_sync = $stdout.sync
252
+ $stdout.sync = true
253
+ self.results = []
254
+ line = []
255
+ begin
256
+ open("| #{cmd}", "r") do |f|
257
+ until f.eof? do
258
+ c = f.getc
259
+ putc c
260
+ line << c
261
+ if c == ?\n then
262
+ self.results << if RUBY_VERSION >= "1.9" then
263
+ line.join
264
+ else
265
+ line.pack "c*"
266
+ end
267
+ line.clear
268
+ end
269
+ end
270
+ end
271
+ ensure
272
+ $stdout.sync = old_sync
273
+ end
274
+ hook :ran_command
275
+ self.results = self.results.join
276
+
277
+ handle_results(self.results)
278
+ end
279
+
280
+ ############################################################
281
+ # Utility Methods, not essential to reading of logic
282
+
283
+ ##
284
+ # Installs a sigint handler.
285
+
286
+ def add_sigint_handler
287
+ trap 'INT' do
288
+ if self.interrupted then
289
+ self.wants_to_quit = true
290
+ else
291
+ unless hook :interrupt then
292
+ puts "Interrupt a second time to quit"
293
+ self.interrupted = true
294
+ Kernel.sleep 1.5
295
+ end
296
+ raise Interrupt, nil # let the run loop catch it
297
+ end
298
+ end
299
+ end
300
+
301
+ ##
302
+ # If there are no files left to test (because they've all passed),
303
+ # then all is good.
304
+
305
+ def all_good
306
+ files_to_test.empty?
307
+ end
308
+
309
+ ##
310
+ # Convert a path in a string, s, into a class name, changing
311
+ # underscores to CamelCase, etc.
312
+
313
+ def path_to_classname(s)
314
+ sep = File::SEPARATOR
315
+ f = s.sub(/^test#{sep}/, '').sub(/\.rb$/, '').split(sep)
316
+ f = f.map { |path| path.split(/_|(\d+)/).map { |seg| seg.capitalize }.join }
317
+ f = f.map { |path| path =~ /^Test/ ? path : "Test#{path}" }
318
+ f.join('::')
319
+ end
320
+
321
+ ##
322
+ # Returns a hash mapping a file name to the known failures for that
323
+ # file.
324
+
325
+ def consolidate_failures(failed)
326
+ filters = new_hash_of_arrays
327
+
328
+ class_map = Hash[*self.find_order.grep(/^test/).map { |f| # TODO: ugly
329
+ [path_to_classname(f), f]
330
+ }.flatten]
331
+ class_map.merge!(self.extra_class_map)
332
+
333
+ failed.each do |method, klass|
334
+ if class_map.has_key? klass then
335
+ filters[class_map[klass]] << method
336
+ else
337
+ output.puts "Unable to map class #{klass} to a file"
338
+ end
339
+ end
340
+
341
+ return filters
342
+ end
343
+
344
+ ##
345
+ # Find the files to process, ignoring temporary files, source
346
+ # configuration management files, etc., and return a Hash mapping
347
+ # filename to modification time.
348
+
349
+ def find_files
350
+ result = {}
351
+ targets = self.find_directories + self.extra_files
352
+ self.find_order.clear
353
+
354
+ targets.each do |target|
355
+ order = []
356
+ Find.find(target) do |f|
357
+ Find.prune if f =~ self.exceptions
358
+
359
+ next if test ?d, f
360
+ next if f =~ /(swp|~|rej|orig)$/ # temporary/patch files
361
+ next if f =~ /\/\.?#/ # Emacs autosave/cvs merge files
362
+
363
+ filename = f.sub(/^\.\//, '')
364
+
365
+ result[filename] = File.stat(filename).mtime rescue next
366
+ order << filename
367
+ end
368
+ self.find_order.push(*order.sort)
369
+ end
370
+
371
+ return result
372
+ end
373
+
374
+ ##
375
+ # Find the files which have been modified, update the recorded
376
+ # timestamps, and use this to update the files to test. Returns true
377
+ # if any file is newer than the previously recorded most recent
378
+ # file.
379
+
380
+ def find_files_to_test(files=find_files)
381
+ updated = files.select { |filename, mtime| self.last_mtime < mtime }
382
+
383
+ p updated if $v unless updated.empty? || self.last_mtime.to_i == 0
384
+
385
+ hook :updated, updated unless updated.empty? || self.last_mtime.to_i == 0
386
+
387
+ updated.map { |f,m| test_files_for(f) }.flatten.uniq.each do |filename|
388
+ self.files_to_test[filename] # creates key with default value
389
+ end
390
+
391
+ if updated.empty? then
392
+ nil
393
+ else
394
+ files.values.max
395
+ end
396
+ end
397
+
398
+ ##
399
+ # Check results for failures, set the "bar" to red or green, and if
400
+ # there are failures record this.
401
+
402
+ def handle_results(results)
403
+ failed = results.scan(self.failed_results_re)
404
+ completed = results =~ self.completed_re
405
+
406
+ self.files_to_test = consolidate_failures failed if completed
407
+
408
+ color = completed && self.files_to_test.empty? ? :green : :red
409
+ hook color unless $TESTING
410
+
411
+ self.tainted = true unless self.files_to_test.empty?
412
+ end
413
+
414
+ ##
415
+ # Lazy accessor for the known_files hash.
416
+
417
+ def known_files
418
+ unless @known_files then
419
+ @known_files = Hash[*find_order.map { |f| [f, true] }.flatten]
420
+ end
421
+ @known_files
422
+ end
423
+
424
+ ##
425
+ # Generate the commands to test the supplied files
426
+
427
+ def make_test_cmd files_to_test
428
+ cmds = []
429
+ full, partial = reorder(files_to_test).partition { |k,v| v.empty? }
430
+
431
+ unless full.empty? then
432
+ classes = full.map {|k,v| k}.flatten.uniq.join(' ')
433
+ cmds << "#{ruby} -I#{libs} -rtest/unit -e \"%w[#{classes}].each { |f| require f }\" | #{unit_diff}"
434
+ end
435
+
436
+ partial.each do |klass, methods|
437
+ regexp = Regexp.union(*methods).source
438
+ cmds << "#{ruby} -I#{libs} #{klass} -n \"/^(#{regexp})$/\" | #{unit_diff}"
439
+ end
440
+
441
+ return cmds.join("#{SEP} ")
442
+ end
443
+
444
+ def new_hash_of_arrays
445
+ Hash.new { |h,k| h[k] = [] }
446
+ end
447
+
448
+ def reorder files_to_test
449
+ case self.order
450
+ when :alpha then
451
+ files_to_test.sort_by { |k,v| k }
452
+ when :reverse then
453
+ files_to_test.sort_by { |k,v| k }.reverse
454
+ when :random then
455
+ max = files_to_test.size
456
+ files_to_test.sort_by { |k,v| rand(max) }
457
+ when :natural then
458
+ (self.find_order & files_to_test.keys).map { |f| [f, files_to_test[f]] }
459
+ else
460
+ raise "unknown order type: #{self.order.inspect}"
461
+ end
462
+ end
463
+
464
+ ##
465
+ # Rerun the tests from cold (reset state)
466
+
467
+ def rerun_all_tests
468
+ reset
469
+ run_tests
470
+
471
+ hook :all_good if all_good
472
+ end
473
+
474
+ ##
475
+ # Clear all state information about test failures and whether
476
+ # interrupts will kill autotest.
477
+
478
+ def reset
479
+ self.files_to_test.clear
480
+ self.find_order.clear
481
+ self.interrupted = false
482
+ self.known_files = nil
483
+ self.last_mtime = T0
484
+ self.tainted = false
485
+ self.wants_to_quit = false
486
+
487
+ hook :reset
488
+ end
489
+
490
+ ##
491
+ # Determine and return the path of the ruby executable.
492
+
493
+ def ruby
494
+ ruby = ENV['RUBY']
495
+ ruby ||= File.join(Config::CONFIG['bindir'],
496
+ Config::CONFIG['ruby_install_name'])
497
+
498
+ ruby.gsub! File::SEPARATOR, File::ALT_SEPARATOR if File::ALT_SEPARATOR
499
+
500
+ return ruby
501
+ end
502
+
503
+ ##
504
+ # Return the name of the file with the tests for filename by finding
505
+ # a +test_mapping+ that matches the file and executing the mapping's
506
+ # proc.
507
+
508
+ def test_files_for(filename)
509
+ result = @test_mappings.find { |file_re, ignored| filename =~ file_re }
510
+
511
+ p :test_file_for => [filename, result.first] if result and $DEBUG
512
+
513
+ result = result.nil? ? [] : Array(result.last.call(filename, $~))
514
+
515
+ output.puts "No tests matched #{filename}" if
516
+ ($v or $TESTING) and result.empty?
517
+
518
+ result.sort.uniq.select { |f| known_files[f] }
519
+ end
520
+
521
+ ##
522
+ # Sleep then look for files to test, until there are some.
523
+
524
+ def wait_for_changes
525
+ hook :waiting
526
+ Kernel.sleep self.sleep until find_files_to_test
527
+ end
528
+
529
+ ############################################################
530
+ # File Mappings:
531
+
532
+ ##
533
+ # Returns all known files in the codebase matching +regexp+.
534
+
535
+ def files_matching regexp
536
+ self.find_order.select { |k| k =~ regexp }
537
+ end
538
+
539
+ ##
540
+ # Adds a file mapping. +regexp+ should match a file path in the
541
+ # codebase. +proc+ is passed a matched filename and
542
+ # Regexp.last_match. +proc+ should return an array of tests to run.
543
+ #
544
+ # For example, if test_helper.rb is modified, rerun all tests:
545
+ #
546
+ # at.add_mapping(/test_helper.rb/) do |f, _|
547
+ # at.files_matching(/^test.*rb$/)
548
+ # end
549
+
550
+ def add_mapping(regexp, &proc)
551
+ @test_mappings << [regexp, proc]
552
+ nil
553
+ end
554
+
555
+ ##
556
+ # Removed a file mapping matching +regexp+.
557
+
558
+ def remove_mapping regexp
559
+ @test_mappings.delete_if do |k,v|
560
+ k == regexp
561
+ end
562
+ nil
563
+ end
564
+
565
+ ##
566
+ # Clears all file mappings. This is DANGEROUS as it entirely
567
+ # disables autotest. You must add at least one file mapping that
568
+ # does a good job of rerunning appropriate tests.
569
+
570
+ def clear_mappings
571
+ @test_mappings.clear
572
+ nil
573
+ end
574
+
575
+ ############################################################
576
+ # Exceptions:
577
+
578
+ ##
579
+ # Adds +regexp+ to the list of exceptions for find_file. This must
580
+ # be called _before_ the exceptions are compiled.
581
+
582
+ def add_exception regexp
583
+ raise "exceptions already compiled" if defined? @exceptions
584
+ @exception_list << regexp
585
+ nil
586
+ end
587
+
588
+ ##
589
+ # Removes +regexp+ to the list of exceptions for find_file. This
590
+ # must be called _before_ the exceptions are compiled.
591
+
592
+ def remove_exception regexp
593
+ raise "exceptions already compiled" if defined? @exceptions
594
+ @exception_list.delete regexp
595
+ nil
596
+ end
597
+
598
+ ##
599
+ # Clears the list of exceptions for find_file. This must be called
600
+ # _before_ the exceptions are compiled.
601
+
602
+ def clear_exceptions
603
+ raise "exceptions already compiled" if defined? @exceptions
604
+ @exception_list.clear
605
+ nil
606
+ end
607
+
608
+ ##
609
+ # Return a compiled regexp of exceptions for find_files or nil if no
610
+ # filtering should take place. This regexp is generated from
611
+ # +exception_list+.
612
+
613
+ def exceptions
614
+ unless defined? @exceptions then
615
+ if @exception_list.empty? then
616
+ @exceptions = nil
617
+ else
618
+ @exceptions = Regexp.union(*@exception_list)
619
+ end
620
+ end
621
+
622
+ @exceptions
623
+ end
624
+
625
+ ############################################################
626
+ # Hooks:
627
+
628
+ ##
629
+ # Call the event hook named +name+, executing all registered hooks
630
+ # until one returns true. Returns false if no hook handled the
631
+ # event.
632
+
633
+ def hook(name, *args)
634
+ deprecated = {
635
+ # none currently
636
+ }
637
+
638
+ if deprecated[name] and not HOOKS[name].empty? then
639
+ warn "hook #{name} has been deprecated, use #{deprecated[name]}"
640
+ end
641
+
642
+ unless HOOKS[name].empty? then
643
+ HOOKS[name].each { |plugin| plugin[self, *args] }
644
+ end
645
+ end
646
+
647
+ ##
648
+ # Add the supplied block to the available hooks, with the given
649
+ # name.
650
+
651
+ def self.add_hook(name, &block)
652
+ HOOKS[name] << block
653
+ end
654
+ end