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/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
|
+
|