ZenTest 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,12 @@
1
+ #!/usr/local/bin/ruby -ws
2
+
3
+ $rails ||= false
4
+
5
+ if $rails then
6
+ require 'rails_autotest'
7
+ RailsAutotest.run
8
+ else
9
+ require 'autotest'
10
+ Autotest.run
11
+ end
12
+
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
@@ -0,0 +1,7 @@
1
+ module Something
2
+ class Thingy
3
+ def do_something
4
+ # ...
5
+ end
6
+ end
7
+ end
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
+