ZenTest 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/autotest.rb ADDED
@@ -0,0 +1,202 @@
1
+ $TESTING = defined? $TESTING
2
+
3
+ require 'find'
4
+
5
+ ##
6
+ # Autotest continuously runs your tests as you work on your project.
7
+ #
8
+ # Autotest periodically scans the files in your project for updates then
9
+ # figures out the appropriate tests to run and runs them. If a test fails
10
+ # Autotest will run just that test until you get it to pass.
11
+ #
12
+ # If you want Autotest to start over from the top, hit ^C. If you want
13
+ # Autotest to quit, hit ^C twice.
14
+ #
15
+ # Autotest uses a simple naming scheme to figure out how to map implementation
16
+ # files to test files following the Test::Unit naming scheme.
17
+ #
18
+ # * Test files must be stored in test/
19
+ # * Test files names must start with test_
20
+ # * Test classes must start with Test
21
+ # * Implementation files must be stored in lib/
22
+ # * Implementation files must match up with a test file named
23
+ # test_.*implementation.rb
24
+
25
+ class Autotest
26
+
27
+ def self.run
28
+ new.run
29
+ end
30
+
31
+ ##
32
+ # Creates a new Autotest. If @exceptions is set, updated_files will use it
33
+ # to reject filenames.
34
+
35
+ def initialize
36
+ @interrupt = false
37
+ @files = Hash.new Time.at(0)
38
+ @exceptions = nil
39
+ end
40
+
41
+ ##
42
+ # Maps failed class +klass+ to test files in +tests+ that have been updated.
43
+
44
+ def failed_test_files(klass, tests)
45
+ failed_klass = klass.sub('Test', '').gsub(/(.)([A-Z])/, '\1_?\2').downcase
46
+ # tests that match this failure
47
+ failed_files = tests.select { |test| test =~ /#{failed_klass}/ }
48
+ # updated implementations that match this failure
49
+ changed_impls = @files.keys.select do |file|
50
+ file =~ %r%^lib.*#{failed_klass}.rb$% and updated? file
51
+ end
52
+ tests_to_run = map_file_names(changed_impls).flatten
53
+ # add updated tests
54
+ failed_files.each { |f| tests_to_run << f if updated? f }
55
+ return tests_to_run.uniq
56
+ end
57
+
58
+ ##
59
+ # Maps implementation files to test files. Returns an Array of one or more
60
+ # Arrays of test filenames.
61
+
62
+ def map_file_names(updated)
63
+ tests = []
64
+
65
+ updated.each do |filename|
66
+ case filename
67
+ when %r%^lib/(?:.*/)?(.*\.rb)$% then
68
+ impl = $1.gsub '_', '_?'
69
+ found = @files.keys.select do |k|
70
+ k =~ %r%^test/.*#{impl}$%
71
+ end
72
+ tests.push(*found)
73
+ when %r%^test/test_% then
74
+ tests << filename # always run tests
75
+ when %r%^(doc|pkg)/% then
76
+ # ignore
77
+ else
78
+ STDERR.puts "Dunno! #{filename}" # What are you trying to pull?
79
+ end
80
+ end
81
+
82
+ return [tests]
83
+ end
84
+
85
+ ##
86
+ # Retests failed tests.
87
+
88
+ def retest_failed(failed, tests)
89
+ # -t and -n includes all tests that match either filter, not tests that
90
+ # match both filters, so figure out which TestCase to run from the filename,
91
+ # and use -n on that.
92
+ until failed.empty? do
93
+ sleep 5 unless $TESTING
94
+
95
+ failed.map! do |method, klass|
96
+ failed_files = failed_test_files klass, tests
97
+ break [method, klass] if failed_files.empty?
98
+ puts "# Rerunning failures: #{failed_files.join ' '}"
99
+ filter = "-n #{method} " unless method == 'default_test'
100
+ cmd = "ruby -Ilib:test -S testrb #{filter}#{failed_files.join ' '}"
101
+ puts "+ #{cmd}"
102
+ system(cmd) ? nil : [method, klass] # clever
103
+ end
104
+
105
+ failed.compact!
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Repeatedly scans for updated files and runs their tests.
111
+
112
+ def run
113
+ trap 'INT' do
114
+ if @interrupt then
115
+ puts "# Ok, you really want to quit, doing so"
116
+ exit
117
+ end
118
+ puts "# hit ^C again to quit"
119
+ sleep 1.5 # give them enough time to hit ^C again
120
+ @interrupt = true # if they hit ^C again,
121
+ raise Interrupt # let the run loop catch it
122
+ end
123
+
124
+ begin
125
+ loop do
126
+ files = updated_files
127
+ test files unless files.empty?
128
+ sleep 5
129
+ end
130
+ rescue Interrupt
131
+ @interrupt = false # they didn't hit ^C in time
132
+ puts "# ok, restarting from the top"
133
+ @files.clear
134
+ retry
135
+ end
136
+ end
137
+
138
+ ##
139
+ # Runs tests for files in +updated+. Implementation files are looked up
140
+ # with map_file_names.
141
+
142
+ def test(updated)
143
+ map_file_names(updated).each do |tests|
144
+ next if tests.empty?
145
+ puts '# Testing updated files'
146
+ cmd = "ruby -Ilib:test -e '#{tests.inspect}.each { |f| load f }'"
147
+ puts "+ #{cmd}"
148
+ results = `#{cmd}`
149
+ puts results
150
+
151
+ if results =~ / 0 failures, 0 errors\Z/ then
152
+ puts '# Passed'
153
+ next
154
+ end
155
+
156
+ failed = results.scan(/^\s+\d+\) (?:Failure|Error):\n(.*?)\((.*?)\)/)
157
+
158
+ if failed.empty? then
159
+ puts '# Test::Unit died, you did a really bad thing, retrying in 10'
160
+ sleep 10
161
+ redo
162
+ end
163
+
164
+ retest_failed failed, tests
165
+ end
166
+
167
+ puts '# All passed'
168
+ end
169
+
170
+ ##
171
+ # Returns true or false if the file has been modified or not. New files are
172
+ # always modified.
173
+
174
+ def updated?(file)
175
+ mtime = File.stat(file).mtime
176
+ updated = @files[file] < mtime
177
+ @files[file] = mtime
178
+ return updated
179
+ end
180
+
181
+ ##
182
+ # Returns names of files that have been modified since updated_files was
183
+ # last run. Files and paths can be ignored by setting @exceptions in
184
+ # initialize.
185
+
186
+ def updated_files
187
+ updated = []
188
+
189
+ Find.find '.' do |f|
190
+ next if File.directory? f
191
+ next if f =~ /(?:swp|~|rej|orig)$/ # temporary/patch files
192
+ next if f =~ %r%/(?:.svn|CVS)/% # version control files
193
+ next if f =~ @exceptions unless @exceptions.nil? # custom exceptions
194
+ f = f.sub(/^\.\//, '') # trim the ./ that Find gives us
195
+ updated << f if updated? f
196
+ end
197
+
198
+ return updated
199
+ end
200
+
201
+ end
202
+
@@ -0,0 +1,57 @@
1
+ require 'autotest'
2
+
3
+ ##
4
+ # RailsAutotest is an Autotest subclass designed for use with Rails projects.
5
+ #
6
+ # To use RailsAutotest pass the -rails flag to autotest.
7
+
8
+ class RailsAutotest < Autotest
9
+
10
+ def initialize # :nodoc:
11
+ super
12
+ @exceptions = %r%(?:^\./(?:db|doc|log|public|script))|(?:.rhtml$)%
13
+ end
14
+
15
+ def map_file_names(updated) # :nodoc:
16
+ model_tests = []
17
+ functional_tests = []
18
+
19
+ updated.each do |filename|
20
+ filename.sub!(/^\.\//, '') # trim the ./ that Find gives us
21
+
22
+ case filename
23
+ when %r%^test/fixtures/(.*)s.yml% then
24
+ model_test = "test/unit/#{$1}_test.rb"
25
+ functional_test = "test/functional/#{$1}_controller_test.rb"
26
+ model_tests << model_test if File.exists? model_test
27
+ functional_tests << functional_test if File.exists? functional_test
28
+ when %r%^test/unit/.*rb$% then
29
+ model_tests << filename
30
+ when %r%^app/models/(.*)\.rb$% then
31
+ model_tests << "test/unit/#{$1}_test.rb"
32
+ when %r%^test/functional/.*\.rb$% then
33
+ functional_tests << filename
34
+ when %r%^app/helpers/application_helper.rb% then
35
+ functional_tests.push(*Dir['test/functional/*_test.rb'])
36
+ when %r%^app/helpers/(.*)_helper.rb% then
37
+ functional_tests << "test/functional/#{$1}_controller_test.rb"
38
+ when %r%^app/controllers/application.rb$% then
39
+ functional_tests << "test/functional/dummy_controller_test.rb"
40
+ when %r%^app/controllers/(.*)\.rb$% then
41
+ functional_tests << "test/functional/#{$1}_test.rb"
42
+ when %r%^app/views/layouts/% then
43
+ when %r%^app/views/(.*)/% then
44
+ functional_tests << "test/functional/#{$1}_controller_test.rb"
45
+ else
46
+ puts "dunno! #{filename}"
47
+ end
48
+ end
49
+
50
+ model_tests.uniq!
51
+ functional_tests.uniq!
52
+
53
+ return model_tests, functional_tests
54
+ end
55
+
56
+ end
57
+
data/lib/unit_diff.rb ADDED
@@ -0,0 +1,200 @@
1
+ require 'tempfile'
2
+
3
+ class Tempfile
4
+ # blatently stolen. Design was poor in Tempfile.
5
+ def self.make_tempname(basename, n=10)
6
+ sprintf('%s%d.%d', basename, $$, n)
7
+ end
8
+
9
+ def self.make_temppath(basename)
10
+ tempname = ""
11
+ n = 1
12
+ begin
13
+ tmpname = File.join('/tmp', make_tempname(basename, n))
14
+ n += 1
15
+ end while File.exist?(tmpname) and n < 100
16
+ tmpname
17
+ end
18
+ end
19
+
20
+ def temp_file(data)
21
+ temp =
22
+ if $k then
23
+ File.new(Tempfile.make_temppath("diff"), "w")
24
+ else
25
+ Tempfile.new("diff")
26
+ end
27
+ count = 0
28
+ data = data.map { |l| '%3d) %s' % [count+=1, l] } if $l
29
+ data = data.join('')
30
+ # unescape newlines, strip <> from entire string
31
+ data = data.gsub(/\\n/, "\n").gsub(/\A</m, '').gsub(/>\Z/m, '').gsub(/0x[a-f0-9]+/m, '0xXXXXXX')
32
+ temp.print data
33
+ temp.puts unless data =~ /\n\Z/m
34
+ temp.flush
35
+ temp.rewind
36
+ temp
37
+ end
38
+
39
+ ##
40
+ # UnitDiff makes reading Test::Unit output easy and fun. Instead of a
41
+ # confusing jumble of text with nearly unnoticable changes like this:
42
+ #
43
+ # 1) Failure:
44
+ # test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
45
+ # <"new GPolyline([\n new GPoint( 47.00000, -122.00000),\n new GPoint( 46.5000
46
+ # 0, -122.50000),\n new GPoint( 46.75000, -122.75000),\n new GPoint( 46.00000,
47
+ # -123.00000)])"> expected but was
48
+ # <"new Gpolyline([\n new GPoint( 47.00000, -122.00000),\n new GPoint( 46.5000
49
+ # 0, -122.50000),\n new GPoint( 46.75000, -122.75000),\n new GPoint( 46.00000,
50
+ # -123.00000)])">.
51
+ #
52
+ #
53
+ # You get an easy-to-read diff output like this:
54
+ #
55
+ # 1) Failure:
56
+ # test_to_gpoints(RouteTest) [test/unit/route_test.rb:29]:
57
+ # 1c1
58
+ # < new GPolyline([
59
+ # ---
60
+ # > new Gpolyline([
61
+ #
62
+ # == Usage
63
+ #
64
+ # test.rb | unit_diff [options]
65
+ # options:
66
+ # -b ignore whitespace differences
67
+ # -c contextual diff
68
+ # -h show usage
69
+ # -k keep temp diff files around
70
+ # -l prefix line numbers on the diffs
71
+ # -u unified diff
72
+ # -v display version
73
+
74
+ class UnitDiff
75
+
76
+ ##
77
+ # Handy wrapper for UnitDiff#unit_diff.
78
+
79
+ def self.unit_diff(input)
80
+ ud = UnitDiff.new
81
+ ud.unit_diff(input)
82
+ end
83
+
84
+ def input(input)
85
+ current = []
86
+ data = []
87
+ data << current
88
+
89
+ # Collect
90
+ input.each_line do |line|
91
+ if line =~ /^\s*$/ or line =~ /^\(?\s*\d+\) (Failure|Error):/ then
92
+ type = $1
93
+ current = []
94
+ data << current
95
+ end
96
+ current << line
97
+ end
98
+ data = data.reject { |o| o == ["\n"] }
99
+ header = data.shift
100
+ footer = data.pop
101
+ return header, data, footer
102
+ end
103
+
104
+ def parse_diff(result)
105
+ header = []
106
+ expect = []
107
+ butwas = []
108
+ found = false
109
+ state = :header
110
+
111
+ until result.empty? do
112
+ case state
113
+ when :header then
114
+ header << result.shift
115
+ state = :expect if result.first =~ /^</
116
+ when :expect then
117
+ state = :butwas if result.first.sub!(/ expected but was/, '')
118
+ expect << result.shift
119
+ when :butwas then
120
+ butwas = result[0..-1]
121
+ result.clear
122
+ else
123
+ raise "unknown state #{state}"
124
+ end
125
+ end
126
+
127
+ return header, expect, nil if butwas.empty?
128
+
129
+ expect.last.chomp!
130
+ expect.first.sub!(/^<\"/, '')
131
+ expect.last.sub!(/\">$/, '')
132
+
133
+ butwas.last.chomp!
134
+ butwas.last.chop! if butwas.last =~ /\.$/
135
+ butwas.first.sub!( /^<\"/, '')
136
+ butwas.last.sub!(/\">$/, '')
137
+
138
+ return header, expect, butwas
139
+ end
140
+
141
+ ##
142
+ # Scans Test::Unit output +input+ looking for comparison failures and makes
143
+ # them easily readable by passing them through diff.
144
+
145
+ def unit_diff(input)
146
+ $b = false unless defined? $b
147
+ $c = false unless defined? $c
148
+ $k = false unless defined? $k
149
+ $l = false unless defined? $l
150
+ $u = false unless defined? $u
151
+
152
+ header, data, footer = self.input(input)
153
+
154
+ header = header.map { |l| l.chomp }
155
+ header << nil unless header.empty?
156
+
157
+ output = [header]
158
+
159
+ # Output
160
+ data.each do |result|
161
+ first = []
162
+ second = []
163
+
164
+ if result.first !~ /Failure/ then
165
+ output.push result.join('')
166
+ next
167
+ end
168
+
169
+ prefix, expect, butwas = parse_diff(result)
170
+
171
+ output.push prefix.compact.map {|line| line.strip}.join("\n")
172
+
173
+ if butwas then
174
+ a = temp_file(expect)
175
+ b = temp_file(butwas)
176
+
177
+ diff_flags = $u ? "-u" : $c ? "-c" : ""
178
+ diff_flags += " -b" if $b
179
+
180
+ result = `diff #{diff_flags} #{a.path} #{b.path}`
181
+ if result.empty? then
182
+ output.push "[no difference--suspect ==]"
183
+ else
184
+ output.push result.map {|line| line.strip}
185
+ end
186
+
187
+ output.push ''
188
+ else
189
+ output.push expect.join('')
190
+ end
191
+ end
192
+
193
+ footer.shift if footer.first.strip.empty?
194
+ output.push footer.compact.map {|line| line.strip}.join("\n")
195
+
196
+ return output.flatten.join("\n")
197
+ end
198
+
199
+ end
200
+