ZenTest 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +104 -0
- data/LinuxJournalArticle.txt +393 -0
- data/Manifest.txt +27 -0
- data/README.txt +123 -0
- data/Rakefile +98 -0
- data/bin/ZenTest +28 -0
- data/bin/autotest +12 -0
- data/bin/unit_diff +40 -0
- data/example.txt +41 -0
- data/example1.rb +7 -0
- data/example2.rb +15 -0
- data/lib/ZenTest.rb +536 -0
- data/lib/autotest.rb +202 -0
- data/lib/rails_autotest.rb +57 -0
- data/lib/unit_diff.rb +200 -0
- data/test/data/normal/lib/photo.rb +0 -0
- data/test/data/normal/test/test_camelcase.rb +0 -0
- data/test/data/normal/test/test_photo.rb +0 -0
- data/test/data/normal/test/test_route.rb +0 -0
- data/test/data/normal/test/test_user.rb +0 -0
- data/test/data/rails/test/fixtures/routes.yml +0 -0
- data/test/data/rails/test/functional/route_controller_test.rb +0 -0
- data/test/data/rails/test/unit/route_test.rb +0 -0
- data/test/test_autotest.rb +179 -0
- data/test/test_rails_autotest.rb +55 -0
- data/test/test_unit_diff.rb +95 -0
- data/test/test_zentest.rb +670 -0
- metadata +103 -0
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
|
+
|