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