ZenTest 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+