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