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/Manifest.txt
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
History.txt
|
2
|
+
LinuxJournalArticle.txt
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
bin/ZenTest
|
7
|
+
bin/autotest
|
8
|
+
bin/unit_diff
|
9
|
+
example.txt
|
10
|
+
example1.rb
|
11
|
+
example2.rb
|
12
|
+
lib/ZenTest.rb
|
13
|
+
lib/autotest.rb
|
14
|
+
lib/rails_autotest.rb
|
15
|
+
lib/unit_diff.rb
|
16
|
+
test/data/normal/lib/photo.rb
|
17
|
+
test/data/normal/test/test_camelcase.rb
|
18
|
+
test/data/normal/test/test_photo.rb
|
19
|
+
test/data/normal/test/test_route.rb
|
20
|
+
test/data/normal/test/test_user.rb
|
21
|
+
test/data/rails/test/fixtures/routes.yml
|
22
|
+
test/data/rails/test/functional/route_controller_test.rb
|
23
|
+
test/data/rails/test/unit/route_test.rb
|
24
|
+
test/test_autotest.rb
|
25
|
+
test/test_rails_autotest.rb
|
26
|
+
test/test_unit_diff.rb
|
27
|
+
test/test_zentest.rb
|
data/README.txt
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
= ZenTest
|
2
|
+
|
3
|
+
* http://www.zenspider.com/ZSS/Products/ZenTest/
|
4
|
+
* support@zenspider.com
|
5
|
+
|
6
|
+
== DESCRIPTION
|
7
|
+
|
8
|
+
ZenTest provides 3 different tools: zentest, unit_diff, and autotest.
|
9
|
+
|
10
|
+
ZenTest scans your target and unit-test code and writes your missing
|
11
|
+
code based on simple naming rules, enabling XP at a much quicker
|
12
|
+
pace. ZenTest only works with Ruby and Test::Unit.
|
13
|
+
|
14
|
+
unit_diff is a command-line filter to diff expected results from
|
15
|
+
actual results and allow you to quickly see exactly what is wrong.
|
16
|
+
|
17
|
+
autotest is a continous testing facility meant to be used during
|
18
|
+
development. As soon as you save a file, autotest will run the
|
19
|
+
corresponding dependent tests.
|
20
|
+
|
21
|
+
There are two strategies intended for ZenTest: test conformance
|
22
|
+
auditing and rapid XP.
|
23
|
+
|
24
|
+
For auditing, ZenTest provides an excellent means of finding methods
|
25
|
+
that have slipped through the testing process. I've run it against my
|
26
|
+
own software and found I missed a lot in a well tested
|
27
|
+
package. Writing those tests found 4 bugs I had no idea existed.
|
28
|
+
|
29
|
+
ZenTest can also be used to evaluate generated code and execute your
|
30
|
+
tests, allowing for very rapid development of both tests and
|
31
|
+
implementation.
|
32
|
+
|
33
|
+
== FEATURES/PROBLEMS
|
34
|
+
|
35
|
+
* Scans your ruby code and tests and generates missing methods for you.
|
36
|
+
* Includes a very helpful filter for Test::Unit output called unit_diff.
|
37
|
+
* Continually and intelligently test only those files you change with autotest.
|
38
|
+
* Includes a LinuxJournal article on testing with ZenTest written by Pat Eyler.
|
39
|
+
|
40
|
+
== SYNOPSYS
|
41
|
+
|
42
|
+
ZenTest MyProject.rb TestMyProject.rb > missing.rb
|
43
|
+
# edit missing.rb and merge appropriate parts into the above files.
|
44
|
+
./TestMyProject.rb | unit_diff
|
45
|
+
# Use unit_diff.rb to show you the actual differences in your failures.
|
46
|
+
|
47
|
+
== RULES
|
48
|
+
|
49
|
+
ZenTest uses the following rules to figure out what code should be
|
50
|
+
generated:
|
51
|
+
|
52
|
+
* Definition:
|
53
|
+
* CUT = Class Under Test
|
54
|
+
* TC = Test Class (for CUT)
|
55
|
+
* TC's name is the same as CUT w/ "Test" prepended at every scope level.
|
56
|
+
* Example: TestA::TestB vs A::B.
|
57
|
+
* CUT method names are used in CT, with "test_" prependend and optional "_ext" extensions for differentiating test case edge boundaries.
|
58
|
+
* Example:
|
59
|
+
* A::B#blah
|
60
|
+
* TestA::TestB#test_blah_normal
|
61
|
+
* TestA::TestB#test_blah_missing_file
|
62
|
+
* All naming conventions are bidirectional with the exception of test extensions.
|
63
|
+
|
64
|
+
== METHOD MAPPING
|
65
|
+
|
66
|
+
Method names are mapped bidirectionally in the following way:
|
67
|
+
|
68
|
+
method test_method
|
69
|
+
method? test_method_eh (too much exposure to Canadians :)
|
70
|
+
method! test_method_bang
|
71
|
+
method= test_method_equals
|
72
|
+
[] test_index
|
73
|
+
* test_times
|
74
|
+
== test_equals2
|
75
|
+
=== test_equals3
|
76
|
+
|
77
|
+
Further, any of the test methods should be able to have arbitrary
|
78
|
+
extensions put on the name to distinguish edge cases:
|
79
|
+
|
80
|
+
method test_method
|
81
|
+
method test_method_simple
|
82
|
+
method test_method_no_network
|
83
|
+
|
84
|
+
To allow for unmapped test methods (ie, non-unit tests), name them:
|
85
|
+
|
86
|
+
test_integration_.*
|
87
|
+
|
88
|
+
== REQUIREMENTS
|
89
|
+
|
90
|
+
* Ruby 1.6+
|
91
|
+
* Test::Unit
|
92
|
+
* Rake or rubygems for install/uninstall
|
93
|
+
|
94
|
+
== INSTALL
|
95
|
+
|
96
|
+
* make test
|
97
|
+
* sudo make install
|
98
|
+
|
99
|
+
== LICENSE
|
100
|
+
|
101
|
+
(The MIT License)
|
102
|
+
|
103
|
+
Copyright (c) 2001-2006 Ryan Davis, Eric Hodel, Zen Spider Software
|
104
|
+
|
105
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
106
|
+
a copy of this software and associated documentation files (the
|
107
|
+
"Software"), to deal in the Software without restriction, including
|
108
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
109
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
110
|
+
permit persons to whom the Software is furnished to do so, subject to
|
111
|
+
the following conditions:
|
112
|
+
|
113
|
+
The above copyright notice and this permission notice shall be
|
114
|
+
included in all copies or substantial portions of the Software.
|
115
|
+
|
116
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
117
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
118
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
119
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
120
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
121
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
122
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
123
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rbconfig'
|
8
|
+
|
9
|
+
$: << 'lib'
|
10
|
+
require 'ZenTest'
|
11
|
+
|
12
|
+
$VERBOSE = nil
|
13
|
+
|
14
|
+
spec = Gem::Specification.new do |s|
|
15
|
+
s.name = 'ZenTest'
|
16
|
+
s.version = ZenTest::VERSION
|
17
|
+
s.authors = ['Ryan Davis', 'Eric Hodel']
|
18
|
+
s.email = 'ryand-ruby@zenspider.com'
|
19
|
+
|
20
|
+
s.files = File.read('Manifest.txt').split($/)
|
21
|
+
s.require_path = 'lib'
|
22
|
+
s.executables = %w[ZenTest unit_diff autotest]
|
23
|
+
|
24
|
+
paragraphs = File.read("README.txt").split(/\n\n+/)
|
25
|
+
s.instance_variable_set "@description", paragraphs[3..9].join("\n\n")
|
26
|
+
s.instance_variable_set "@summary", paragraphs[11]
|
27
|
+
|
28
|
+
if $DEBUG then
|
29
|
+
puts "ZenTest #{s.version}"
|
30
|
+
puts
|
31
|
+
puts s.summary
|
32
|
+
puts
|
33
|
+
puts s.description
|
34
|
+
end
|
35
|
+
|
36
|
+
s.files = IO.readlines("Manifest.txt").map {|f| f.chomp }
|
37
|
+
s.homepage = "http://www.zenspider.com/ZSS/Products/ZenTest/"
|
38
|
+
s.rubyforge_project = "zentest"
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'Run tests'
|
42
|
+
task :default => :test
|
43
|
+
|
44
|
+
desc 'Run tests'
|
45
|
+
Rake::TestTask.new :test do |t|
|
46
|
+
t.libs << 'test'
|
47
|
+
t.verbose = true
|
48
|
+
end
|
49
|
+
|
50
|
+
desc 'Update Manifest.txt'
|
51
|
+
task :update_manifest => :clean do
|
52
|
+
sh "p4 open Manifest.txt; find . -type f | sed -e 's%./%%' | sort > Manifest.txt"
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'Generate RDoc'
|
56
|
+
Rake::RDocTask.new :rdoc do |rd|
|
57
|
+
rd.rdoc_dir = 'doc'
|
58
|
+
rd.rdoc_files.add 'lib', 'README.txt', 'History.txt',
|
59
|
+
'LinuxJournalArticle.txt'
|
60
|
+
rd.main = 'README.txt'
|
61
|
+
rd.options << '-d' if `which dot` =~ /\/dot/
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'Build Gem'
|
65
|
+
Rake::GemPackageTask.new spec do |pkg|
|
66
|
+
pkg.need_tar = true
|
67
|
+
end
|
68
|
+
|
69
|
+
$prefix = ENV['PREFIX'] || Config::CONFIG['prefix']
|
70
|
+
$bin = File.join($prefix, 'bin')
|
71
|
+
$lib = Config::CONFIG['sitelibdir']
|
72
|
+
$bins = %w(ZenTest autotest unit_diff)
|
73
|
+
$libs = %w(ZenTest.rb autotest.rb rails_autotest.rb unit_diff.rb)
|
74
|
+
|
75
|
+
task :install do
|
76
|
+
$bins.each do |f|
|
77
|
+
install File.join("bin", f), $bin, :mode => 0555
|
78
|
+
end
|
79
|
+
|
80
|
+
$libs.each do |f|
|
81
|
+
install File.join("lib", f), $lib, :mode => 0444
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
task :uninstall do
|
86
|
+
$bins.each do |f|
|
87
|
+
rm_f File.join($bin, f)
|
88
|
+
end
|
89
|
+
|
90
|
+
$libs.each do |f|
|
91
|
+
rm_f File.join($lib, f)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
desc 'Clean up'
|
96
|
+
task :clean => [ :clobber_rdoc, :clobber_package ] do
|
97
|
+
rm_rf %w(*~ doc)
|
98
|
+
end
|
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/bin/autotest
ADDED
data/bin/unit_diff
ADDED
@@ -0,0 +1,40 @@
|
|
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
|
+
|
19
|
+
############################################################
|
20
|
+
|
21
|
+
UNIT_DIFF_VERSION = '1.1.0'
|
22
|
+
|
23
|
+
if defined? $v then
|
24
|
+
puts "#{File.basename $0} v. #{UNIT_DIFF_VERSION}"
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
if defined? $h then
|
29
|
+
File.open(File.basename($0)) do |f|
|
30
|
+
begin; end until f.readline =~ /usage:/
|
31
|
+
f.readline
|
32
|
+
while line = f.readline and line.sub!(/^# ?/, '')
|
33
|
+
$stderr.puts line
|
34
|
+
end
|
35
|
+
end
|
36
|
+
exit 0
|
37
|
+
end
|
38
|
+
|
39
|
+
puts UnitDiff.unit_diff(ARGF)
|
40
|
+
|
data/example.txt
ADDED
@@ -0,0 +1,41 @@
|
|
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
|
+
|
9
|
+
We NEED to make testing as easy as possible to get them testing.
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Class Under Test Test Class
|
26
|
+
######################################################################
|
27
|
+
|
28
|
+
module Something module TestSomething
|
29
|
+
class Thingy class TestThingy
|
30
|
+
def do_something def test_do_something_normal
|
31
|
+
# ... thingy = Thingy.new
|
32
|
+
end result = thingy.do_something
|
33
|
+
end assert(result.blahblah)
|
34
|
+
end end
|
35
|
+
def test_do_something_edgecase
|
36
|
+
thingy = Thingy.new
|
37
|
+
result = thingy.do_something
|
38
|
+
assert(result.blahblah)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/example1.rb
ADDED
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
|
+
|
data/lib/ZenTest.rb
ADDED
@@ -0,0 +1,536 @@
|
|
1
|
+
$stdlib = {}
|
2
|
+
ObjectSpace.each_object(Module) { |m| $stdlib[m.name] = true }
|
3
|
+
|
4
|
+
$:.unshift( *$I.split(/:/) ) if defined? $I and String === $I
|
5
|
+
$r = false unless defined? $r # reverse mapping for testclass names
|
6
|
+
|
7
|
+
if $r then
|
8
|
+
$-w = false # rails is retarded
|
9
|
+
$: << 'config'
|
10
|
+
require 'environment'
|
11
|
+
end
|
12
|
+
|
13
|
+
$ZENTEST = true
|
14
|
+
$TESTING = true
|
15
|
+
|
16
|
+
require 'test/unit/testcase' # helps required modules
|
17
|
+
|
18
|
+
class Module
|
19
|
+
|
20
|
+
def zentest
|
21
|
+
at_exit { ZenTest.autotest(self) }
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class ZenTest
|
27
|
+
|
28
|
+
VERSION = '3.0.0'
|
29
|
+
|
30
|
+
if $TESTING then
|
31
|
+
attr_reader :missing_methods
|
32
|
+
attr_accessor :test_klasses
|
33
|
+
attr_accessor :klasses
|
34
|
+
attr_accessor :inherited_methods
|
35
|
+
else
|
36
|
+
def missing_methods; raise "Something is wack"; end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
@result = []
|
41
|
+
@test_klasses = {}
|
42
|
+
@klasses = {}
|
43
|
+
@error_count = 0
|
44
|
+
@inherited_methods = Hash.new { |h,k| h[k] = {} }
|
45
|
+
# key = klassname, val = hash of methods => true
|
46
|
+
@missing_methods = Hash.new { |h,k| h[k] = {} }
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_file(file)
|
50
|
+
puts "# loading #{file} // #{$0}" if $DEBUG
|
51
|
+
|
52
|
+
unless file == $0 then
|
53
|
+
begin
|
54
|
+
require "#{file}"
|
55
|
+
rescue LoadError => err
|
56
|
+
puts "Could not load #{file}: #{err}"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
puts "# Skipping loading myself (#{file})" if $DEBUG
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_class(klassname)
|
64
|
+
begin
|
65
|
+
klass = Module.const_get(klassname.intern)
|
66
|
+
puts "# found class #{klass.name}" if $DEBUG
|
67
|
+
rescue NameError
|
68
|
+
ObjectSpace.each_object(Class) do |cls|
|
69
|
+
if cls.name =~ /(^|::)#{klassname}$/ then
|
70
|
+
klass = cls
|
71
|
+
klassname = cls.name
|
72
|
+
break
|
73
|
+
end
|
74
|
+
end
|
75
|
+
puts "# searched and found #{klass.name}" if klass and $DEBUG
|
76
|
+
end
|
77
|
+
|
78
|
+
if klass.nil? and not $TESTING then
|
79
|
+
puts "Could not figure out how to get #{klassname}..."
|
80
|
+
puts "Report to support-zentest@zenspider.com w/ relevant source"
|
81
|
+
end
|
82
|
+
|
83
|
+
return klass
|
84
|
+
end
|
85
|
+
|
86
|
+
def get_methods_for(klass, full=false)
|
87
|
+
klass = self.get_class(klass) if klass.kind_of? String
|
88
|
+
|
89
|
+
# WTF? public_instance_methods: default vs true vs false = 3 answers
|
90
|
+
public_methods = klass.public_instance_methods(false)
|
91
|
+
klass_methods = klass.singleton_methods(full)
|
92
|
+
klass_methods -= Class.public_methods(true)
|
93
|
+
klass_methods -= %w(suite new)
|
94
|
+
klass_methods = klass_methods.map { |m| "self." + m }
|
95
|
+
public_methods += klass_methods
|
96
|
+
public_methods -= Kernel.methods unless full
|
97
|
+
klassmethods = {}
|
98
|
+
public_methods.each do |meth|
|
99
|
+
puts "# found method #{meth}" if $DEBUG
|
100
|
+
klassmethods[meth] = true
|
101
|
+
end
|
102
|
+
|
103
|
+
return klassmethods
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_inherited_methods_for(klass, full)
|
107
|
+
klass = self.get_class(klass) if klass.kind_of? String
|
108
|
+
|
109
|
+
klassmethods = {}
|
110
|
+
if (klass.class.method_defined?(:superclass)) then
|
111
|
+
superklass = klass.superclass
|
112
|
+
if superklass then
|
113
|
+
the_methods = superklass.instance_methods(true)
|
114
|
+
|
115
|
+
# generally we don't test Object's methods...
|
116
|
+
unless full then
|
117
|
+
the_methods -= Object.instance_methods(true)
|
118
|
+
the_methods -= Kernel.methods # FIX (true) - check 1.6 vs 1.8
|
119
|
+
end
|
120
|
+
|
121
|
+
the_methods.each do |meth|
|
122
|
+
klassmethods[meth] = true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
return klassmethods
|
127
|
+
end
|
128
|
+
|
129
|
+
def is_test_class(klass)
|
130
|
+
klass = klass.to_s
|
131
|
+
klasspath = klass.split(/::/)
|
132
|
+
a_bad_classpath = klasspath.find do |s| s !~ ($r ? /Test$/ : /^Test/) end
|
133
|
+
return a_bad_classpath.nil?
|
134
|
+
end
|
135
|
+
|
136
|
+
def convert_class_name(name)
|
137
|
+
name = name.to_s
|
138
|
+
|
139
|
+
if self.is_test_class(name) then
|
140
|
+
if $r then
|
141
|
+
name = name.gsub(/Test($|::)/, '\1') # FooTest::BlahTest => Foo::Blah
|
142
|
+
else
|
143
|
+
name = name.gsub(/(^|::)Test/, '\1') # TestFoo::TestBlah => Foo::Blah
|
144
|
+
end
|
145
|
+
else
|
146
|
+
if $r then
|
147
|
+
name = name.gsub(/($|::)/, 'Test\1') # Foo::Blah => FooTest::BlahTest
|
148
|
+
else
|
149
|
+
name = name.gsub(/(^|::)/, '\1Test') # Foo::Blah => TestFoo::TestBlah
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
return name
|
154
|
+
end
|
155
|
+
|
156
|
+
def process_class(klassname, full=false)
|
157
|
+
klass = self.get_class(klassname)
|
158
|
+
raise "Couldn't get class for #{klassname}" if klass.nil?
|
159
|
+
klassname = klass.name # refetch to get full name
|
160
|
+
|
161
|
+
is_test_class = self.is_test_class(klassname)
|
162
|
+
target = is_test_class ? @test_klasses : @klasses
|
163
|
+
|
164
|
+
# record public instance methods JUST in this class
|
165
|
+
target[klassname] = self.get_methods_for(klass, full)
|
166
|
+
|
167
|
+
# record ALL instance methods including superclasses (minus Object)
|
168
|
+
@inherited_methods[klassname] = self.get_inherited_methods_for(klass, full)
|
169
|
+
return klassname
|
170
|
+
end
|
171
|
+
|
172
|
+
def scan_files(*files)
|
173
|
+
assert_count = Hash.new(0)
|
174
|
+
method_count = Hash.new(0)
|
175
|
+
klassname = nil
|
176
|
+
|
177
|
+
files.each do |path|
|
178
|
+
is_loaded = false
|
179
|
+
|
180
|
+
# if reading stdin, slurp the whole thing at once
|
181
|
+
file = (path == "-" ? $stdin.read : File.new(path))
|
182
|
+
|
183
|
+
file.each_line do |line|
|
184
|
+
|
185
|
+
if klassname then
|
186
|
+
case line
|
187
|
+
when /^\s*def/ then
|
188
|
+
method_count[klassname] += 1
|
189
|
+
when /assert|flunk/ then
|
190
|
+
assert_count[klassname] += 1
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
if line =~ /^\s*(?:class|module)\s+([\w:]+)/ then
|
195
|
+
klassname = $1
|
196
|
+
|
197
|
+
if line =~ /\#\s*ZenTest SKIP/ then
|
198
|
+
klassname = nil
|
199
|
+
next
|
200
|
+
end
|
201
|
+
|
202
|
+
full = false
|
203
|
+
if line =~ /\#\s*ZenTest FULL/ then
|
204
|
+
full = true
|
205
|
+
end
|
206
|
+
|
207
|
+
unless is_loaded then
|
208
|
+
unless path == "-" then
|
209
|
+
self.load_file(path)
|
210
|
+
else
|
211
|
+
eval file, TOPLEVEL_BINDING
|
212
|
+
end
|
213
|
+
is_loaded = true
|
214
|
+
end
|
215
|
+
|
216
|
+
begin
|
217
|
+
klassname = self.process_class(klassname, full)
|
218
|
+
rescue
|
219
|
+
puts "# Couldn't find class for name #{klassname}"
|
220
|
+
next
|
221
|
+
end
|
222
|
+
|
223
|
+
# Special Case: ZenTest is already loaded since we are running it
|
224
|
+
if klassname == "TestZenTest" then
|
225
|
+
klassname = "ZenTest"
|
226
|
+
self.process_class(klassname, false)
|
227
|
+
end
|
228
|
+
|
229
|
+
end # if /class/
|
230
|
+
end # IO.foreach
|
231
|
+
end # files
|
232
|
+
|
233
|
+
result = []
|
234
|
+
method_count.each_key do |classname|
|
235
|
+
|
236
|
+
entry = {}
|
237
|
+
|
238
|
+
next if is_test_class(classname)
|
239
|
+
testclassname = convert_class_name(classname)
|
240
|
+
a_count = assert_count[testclassname]
|
241
|
+
m_count = method_count[classname]
|
242
|
+
ratio = a_count.to_f / m_count.to_f * 100.0
|
243
|
+
|
244
|
+
entry['n'] = classname
|
245
|
+
entry['r'] = ratio
|
246
|
+
entry['a'] = a_count
|
247
|
+
entry['m'] = m_count
|
248
|
+
|
249
|
+
result.push entry
|
250
|
+
end
|
251
|
+
|
252
|
+
sorted_results = result.sort { |a,b| b['r'] <=> a['r'] }
|
253
|
+
|
254
|
+
@result.push sprintf("# %25s: %4s / %4s = %6s%%", "classname", "asrt", "meth", "ratio")
|
255
|
+
sorted_results.each do |e|
|
256
|
+
@result.push sprintf("# %25s: %4d / %4d = %6.2f%%", e['n'], e['a'], e['m'], e['r'])
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def add_missing_method(klassname, methodname)
|
261
|
+
@result.push "# ERROR method #{klassname}\##{methodname} does not exist (1)" if $DEBUG and not $TESTING
|
262
|
+
@error_count += 1
|
263
|
+
@missing_methods[klassname][methodname] = true
|
264
|
+
end
|
265
|
+
|
266
|
+
@@orig_method_map = {
|
267
|
+
'!' => 'bang',
|
268
|
+
'%' => 'percent',
|
269
|
+
'&' => 'and',
|
270
|
+
'*' => 'times',
|
271
|
+
'**' => 'times2',
|
272
|
+
'+' => 'plus',
|
273
|
+
'-' => 'minus',
|
274
|
+
'/' => 'div',
|
275
|
+
'<' => 'lt',
|
276
|
+
'<=' => 'lte',
|
277
|
+
'<=>' => 'spaceship',
|
278
|
+
"<\<" => 'lt2',
|
279
|
+
'==' => 'equals2',
|
280
|
+
'===' => 'equals3',
|
281
|
+
'=~' => 'equalstilde',
|
282
|
+
'>' => 'gt',
|
283
|
+
'>=' => 'ge',
|
284
|
+
'>>' => 'gt2',
|
285
|
+
'@+' => 'unary_plus',
|
286
|
+
'@-' => 'unary_minus',
|
287
|
+
'[]' => 'index',
|
288
|
+
'[]=' => 'index_equals',
|
289
|
+
'^' => 'carat',
|
290
|
+
'|' => 'or',
|
291
|
+
'~' => 'tilde',
|
292
|
+
}
|
293
|
+
|
294
|
+
@@method_map = @@orig_method_map.merge(@@orig_method_map.invert)
|
295
|
+
|
296
|
+
def normal_to_test(name)
|
297
|
+
name = name.dup # wtf?
|
298
|
+
is_cls_method = name.sub!(/^self\./, '')
|
299
|
+
name = @@method_map[name] if @@method_map.has_key? name
|
300
|
+
name = name.sub(/=$/, '_equals')
|
301
|
+
name = name.sub(/\?$/, '_eh')
|
302
|
+
name = name.sub(/\!$/, '_bang')
|
303
|
+
name = "class_" + name if is_cls_method
|
304
|
+
"test_#{name}"
|
305
|
+
end
|
306
|
+
|
307
|
+
def test_to_normal(name, klassname=nil)
|
308
|
+
known_methods = (@inherited_methods[klassname] || {}).keys.sort.reverse
|
309
|
+
|
310
|
+
mapped_re = @@orig_method_map.values.sort_by { |k| k.length }.map {|s| Regexp.escape(s)}.reverse.join("|")
|
311
|
+
known_methods_re = known_methods.map {|s| Regexp.escape(s)}.join("|")
|
312
|
+
|
313
|
+
name = name.sub(/^test_/, '')
|
314
|
+
name = name.sub(/_equals/, '=') unless name =~ /index/
|
315
|
+
name = name.sub(/_bang.*$/, '!') # FIX: deal w/ extensions separately
|
316
|
+
name = name.sub(/_eh/, '?')
|
317
|
+
is_cls_method = name.sub!(/^class_/, '')
|
318
|
+
name = name.sub(/^(#{mapped_re})(.*)$/) {$1}
|
319
|
+
name = name.sub(/^(#{known_methods_re})(.*)$/) {$1} unless known_methods_re.empty?
|
320
|
+
|
321
|
+
# look up in method map
|
322
|
+
name = @@method_map[name] if @@method_map.has_key? name
|
323
|
+
|
324
|
+
name = 'self.' + name if is_cls_method
|
325
|
+
|
326
|
+
name
|
327
|
+
end
|
328
|
+
|
329
|
+
def analyze_impl(klassname)
|
330
|
+
testklassname = self.convert_class_name(klassname)
|
331
|
+
if @test_klasses[testklassname] then
|
332
|
+
methods = @klasses[klassname]
|
333
|
+
testmethods = @test_klasses[testklassname]
|
334
|
+
|
335
|
+
# check that each method has a test method
|
336
|
+
@klasses[klassname].each_key do | methodname |
|
337
|
+
testmethodname = normal_to_test(methodname)
|
338
|
+
unless testmethods[testmethodname] then
|
339
|
+
begin
|
340
|
+
unless testmethods.keys.find { |m| m =~ /#{testmethodname}(_\w+)+$/ } then
|
341
|
+
self.add_missing_method(testklassname, testmethodname)
|
342
|
+
end
|
343
|
+
rescue RegexpError => e
|
344
|
+
puts "# ERROR trying to use '#{testmethodname}' as a regex. Look at #{klassname}.#{methodname}"
|
345
|
+
end
|
346
|
+
end # testmethods[testmethodname]
|
347
|
+
end # @klasses[klassname].each_key
|
348
|
+
else # ! @test_klasses[testklassname]
|
349
|
+
puts "# ERROR test class #{testklassname} does not exist" if $DEBUG
|
350
|
+
@error_count += 1
|
351
|
+
|
352
|
+
@klasses[klassname].keys.each do | methodname |
|
353
|
+
self.add_missing_method(testklassname, normal_to_test(methodname))
|
354
|
+
end
|
355
|
+
end # @test_klasses[testklassname]
|
356
|
+
end
|
357
|
+
|
358
|
+
def analyze_test(testklassname)
|
359
|
+
klassname = self.convert_class_name(testklassname)
|
360
|
+
|
361
|
+
# CUT might be against a core class, if so, slurp it and analyze it
|
362
|
+
if $stdlib[klassname] then
|
363
|
+
self.process_class(klassname, true)
|
364
|
+
self.analyze_impl(klassname)
|
365
|
+
end
|
366
|
+
|
367
|
+
if @klasses[klassname] then
|
368
|
+
methods = @klasses[klassname]
|
369
|
+
testmethods = @test_klasses[testklassname]
|
370
|
+
|
371
|
+
# check that each test method has a method
|
372
|
+
testmethods.each_key do | testmethodname |
|
373
|
+
if testmethodname =~ /^test_(?!integration_)/ then
|
374
|
+
|
375
|
+
# try the current name
|
376
|
+
methodname = test_to_normal(testmethodname, klassname)
|
377
|
+
orig_name = methodname.dup
|
378
|
+
|
379
|
+
found = false
|
380
|
+
until methodname == "" or methods[methodname] or @inherited_methods[klassname][methodname] do
|
381
|
+
# try the name minus an option (ie mut_opt1 -> mut)
|
382
|
+
if methodname.sub!(/_[^_]+$/, '') then
|
383
|
+
if methods[methodname] or @inherited_methods[klassname][methodname] then
|
384
|
+
found = true
|
385
|
+
end
|
386
|
+
else
|
387
|
+
break # no more substitutions will take place
|
388
|
+
end
|
389
|
+
end # methodname == "" or ...
|
390
|
+
|
391
|
+
unless found or methods[methodname] or methodname == "initialize" then
|
392
|
+
self.add_missing_method(klassname, orig_name)
|
393
|
+
end
|
394
|
+
|
395
|
+
else # not a test_.* method
|
396
|
+
unless testmethodname =~ /^util_/ then
|
397
|
+
puts "# WARNING Skipping #{testklassname}\##{testmethodname}" if $DEBUG
|
398
|
+
end
|
399
|
+
end # testmethodname =~ ...
|
400
|
+
end # testmethods.each_key
|
401
|
+
else # ! @klasses[klassname]
|
402
|
+
puts "# ERROR class #{klassname} does not exist" if $DEBUG
|
403
|
+
@error_count += 1
|
404
|
+
|
405
|
+
@test_klasses[testklassname].keys.each do |testmethodname|
|
406
|
+
@missing_methods[klassname][test_to_normal(testmethodname)] = true
|
407
|
+
end
|
408
|
+
end # @klasses[klassname]
|
409
|
+
end
|
410
|
+
|
411
|
+
def analyze
|
412
|
+
# walk each known class and test that each method has a test method
|
413
|
+
@klasses.each_key do |klassname|
|
414
|
+
self.analyze_impl(klassname)
|
415
|
+
end
|
416
|
+
|
417
|
+
# now do it in the other direction...
|
418
|
+
@test_klasses.each_key do |testklassname|
|
419
|
+
self.analyze_test(testklassname)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def generate_code
|
424
|
+
|
425
|
+
# @result.unshift "# run against: #{files.join(', ')}" if $DEBUG
|
426
|
+
@result.unshift "# Code Generated by ZenTest v. #{VERSION}"
|
427
|
+
|
428
|
+
if $DEBUG then
|
429
|
+
@result.push "# found classes: #{@klasses.keys.join(', ')}"
|
430
|
+
@result.push "# found test classes: #{@test_klasses.keys.join(', ')}"
|
431
|
+
end
|
432
|
+
|
433
|
+
if @missing_methods.size > 0 then
|
434
|
+
@result.push ""
|
435
|
+
@result.push "require 'test/unit' unless defined? $ZENTEST and $ZENTEST"
|
436
|
+
@result.push ""
|
437
|
+
end
|
438
|
+
|
439
|
+
indentunit = " "
|
440
|
+
|
441
|
+
@missing_methods.keys.sort.each do |fullklasspath|
|
442
|
+
|
443
|
+
methods = @missing_methods[fullklasspath]
|
444
|
+
cls_methods = methods.keys.grep(/^(self\.|test_class_)/)
|
445
|
+
methods.delete_if {|k,v| cls_methods.include? k }
|
446
|
+
|
447
|
+
next if methods.empty? and cls_methods.empty?
|
448
|
+
|
449
|
+
indent = 0
|
450
|
+
is_test_class = self.is_test_class(fullklasspath)
|
451
|
+
klasspath = fullklasspath.split(/::/)
|
452
|
+
klassname = klasspath.pop
|
453
|
+
|
454
|
+
klasspath.each do | modulename |
|
455
|
+
m = self.get_class(modulename)
|
456
|
+
type = m.nil? ? "module" : m.class.name.downcase
|
457
|
+
@result.push indentunit*indent + "#{type} #{modulename}"
|
458
|
+
indent += 1
|
459
|
+
end
|
460
|
+
@result.push indentunit*indent + "class #{klassname}" + (is_test_class ? " < Test::Unit::TestCase" : '')
|
461
|
+
indent += 1
|
462
|
+
|
463
|
+
meths = []
|
464
|
+
|
465
|
+
cls_methods.sort.each do |method|
|
466
|
+
meth = []
|
467
|
+
meth.push indentunit*indent + "def #{method}"
|
468
|
+
meth.last << "(*args)" unless method =~ /^test/
|
469
|
+
indent += 1
|
470
|
+
meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{method}'"
|
471
|
+
indent -= 1
|
472
|
+
meth.push indentunit*indent + "end"
|
473
|
+
meths.push meth.join("\n")
|
474
|
+
end
|
475
|
+
|
476
|
+
methods.keys.sort.each do |method|
|
477
|
+
next if method =~ /pretty_print/
|
478
|
+
meth = []
|
479
|
+
meth.push indentunit*indent + "def #{method}"
|
480
|
+
meth.last << "(*args)" unless method =~ /^test/
|
481
|
+
indent += 1
|
482
|
+
meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{method}'"
|
483
|
+
indent -= 1
|
484
|
+
meth.push indentunit*indent + "end"
|
485
|
+
meths.push meth.join("\n")
|
486
|
+
end
|
487
|
+
|
488
|
+
@result.push meths.join("\n\n")
|
489
|
+
|
490
|
+
indent -= 1
|
491
|
+
@result.push indentunit*indent + "end"
|
492
|
+
klasspath.each do | modulename |
|
493
|
+
indent -= 1
|
494
|
+
@result.push indentunit*indent + "end"
|
495
|
+
end
|
496
|
+
@result.push ''
|
497
|
+
end
|
498
|
+
|
499
|
+
@result.push "# Number of errors detected: #{@error_count}"
|
500
|
+
@result.push ''
|
501
|
+
end
|
502
|
+
|
503
|
+
def result
|
504
|
+
return @result.join("\n")
|
505
|
+
end
|
506
|
+
|
507
|
+
def self.fix(*files)
|
508
|
+
zentest = ZenTest.new
|
509
|
+
zentest.scan_files(*files)
|
510
|
+
zentest.analyze
|
511
|
+
zentest.generate_code
|
512
|
+
return zentest.result
|
513
|
+
end
|
514
|
+
|
515
|
+
def self.autotest(*klasses)
|
516
|
+
zentest = ZenTest.new
|
517
|
+
klasses.each do |klass|
|
518
|
+
zentest.process_class(klass)
|
519
|
+
end
|
520
|
+
|
521
|
+
zentest.analyze
|
522
|
+
|
523
|
+
zentest.missing_methods.each do |klass,methods|
|
524
|
+
methods.each do |method,x|
|
525
|
+
warn "autotest generating #{klass}##{method}"
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
zentest.generate_code
|
530
|
+
code = zentest.result
|
531
|
+
puts code if $DEBUG
|
532
|
+
|
533
|
+
Object.class_eval code
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|