In Files

Parent

Included Modules

ZenTest

ZenTest scans your target and unit-test code and writes your missing code based on simple naming rules, enabling XP at a much quicker pace. ZenTest only works with Ruby and Test::Unit.

RULES

ZenTest uses the following rules to figure out what code should be generated:

See ZenTestMapping for documentation on method naming.

Constants

VERSION

Public Class Methods

autotest(*klasses) click to toggle source

Process all the supplied classes for methods etc, and analyse the results. Generate the skeletal code and eval it to put the methods into the runtime environment.

     # File lib/zentest.rb, line 556
556:   def self.autotest(*klasses)
557:     zentest = ZenTest.new
558:     klasses.each do |klass|
559:       zentest.process_class(klass)
560:     end
561: 
562:     zentest.analyze
563: 
564:     zentest.missing_methods.each do |klass,methods|
565:       methods.each do |method,x|
566:         warn "autotest generating #{klass}##{method}"
567:       end
568:     end
569: 
570:     zentest.generate_code
571:     code = zentest.result
572:     puts code if $DEBUG
573: 
574:     Object.class_eval code
575:   end
fix(*files) click to toggle source

Runs ZenTest over all the supplied files so that they are analysed and the missing methods have skeleton code written.

     # File lib/zentest.rb, line 544
544:   def self.fix(*files)
545:     zentest = ZenTest.new
546:     zentest.scan_files(*files)
547:     zentest.analyze
548:     zentest.generate_code
549:     return zentest.result
550:   end
new() click to toggle source
    # File lib/zentest.rb, line 69
69:   def initialize
70:     @result = []
71:     @test_klasses = {}
72:     @klasses = {}
73:     @error_count = 0
74:     @inherited_methods = Hash.new { |h,k| h[k] = {} }
75:     # key = klassname, val = hash of methods => true
76:     @missing_methods = Hash.new { |h,k| h[k] = {} }
77:   end

Public Instance Methods

add_missing_method(klassname, methodname) click to toggle source

Adds a missing method to the collected results.

     # File lib/zentest.rb, line 323
323:   def add_missing_method(klassname, methodname)
324:     @result.push "# ERROR method #{klassname}\##{methodname} does not exist (1)" if $DEBUG and not $TESTING
325:     @error_count += 1
326:     @missing_methods[klassname][methodname] = true
327:   end
analyze() click to toggle source

Walk each known class and test that each method has a test method Then do it in the other direction...

     # File lib/zentest.rb, line 453
453:   def analyze
454:     # walk each known class and test that each method has a test method
455:     @klasses.each_key do |klassname|
456:       self.analyze_impl(klassname)
457:     end
458: 
459:     # now do it in the other direction...
460:     @test_klasses.each_key do |testklassname|
461:       self.analyze_test(testklassname)
462:     end
463:   end
analyze_impl(klassname) click to toggle source

Checks, for the given class klassname, that each method has a corrsponding test method. If it doesn’t this is added to the information for that class

     # File lib/zentest.rb, line 339
339:   def analyze_impl(klassname)
340:     testklassname = self.convert_class_name(klassname)
341:     if @test_klasses[testklassname] then
342:       methods, testmethods = methods_and_tests(klassname,testklassname)
343: 
344:       # check that each method has a test method
345:       @klasses[klassname].each_key do | methodname |
346:         testmethodname = normal_to_test(methodname)
347:         unless testmethods[testmethodname] then
348:           begin
349:             unless testmethods.keys.find { |m| m =~ /#{testmethodname}(_\w+)+$/ } then
350:               self.add_missing_method(testklassname, testmethodname)
351:             end
352:           rescue RegexpError => e
353:             puts "# ERROR trying to use '#{testmethodname}' as a regex. Look at #{klassname}.#{methodname}"
354:           end
355:         end # testmethods[testmethodname]
356:       end # @klasses[klassname].each_key
357:     else # ! @test_klasses[testklassname]
358:       puts "# ERROR test class #{testklassname} does not exist" if $DEBUG
359:       @error_count += 1
360: 
361:       @klasses[klassname].keys.each do | methodname |
362:         self.add_missing_method(testklassname, normal_to_test(methodname))
363:       end
364:     end # @test_klasses[testklassname]
365:   end
analyze_test(testklassname) click to toggle source

For the given test class testklassname, ensure that all the test methods have corresponding (normal) methods. If not, add them to the information about that class.

     # File lib/zentest.rb, line 370
370:   def analyze_test(testklassname)
371:     klassname = self.convert_class_name(testklassname)
372: 
373:     # CUT might be against a core class, if so, slurp it and analyze it
374:     if $stdlib[klassname] then
375:       self.process_class(klassname, true)
376:       self.analyze_impl(klassname)
377:     end
378: 
379:     if @klasses[klassname] then
380:       methods, testmethods = methods_and_tests(klassname,testklassname)
381: 
382:       # check that each test method has a method
383:       testmethods.each_key do | testmethodname |
384:         if testmethodname =~ /^test_(?!integration_)/ then
385: 
386:           # try the current name
387:           methodname = test_to_normal(testmethodname, klassname)
388:           orig_name = methodname.dup
389: 
390:           found = false
391:           until methodname == "" or methods[methodname] or @inherited_methods[klassname][methodname] do
392:               # try the name minus an option (ie mut_opt1 -> mut)
393:             if methodname.sub!(/_[^_]+$/, '') then
394:               if methods[methodname] or @inherited_methods[klassname][methodname] then
395:                 found = true
396:               end
397:             else
398:               break # no more substitutions will take place
399:             end
400:           end # methodname == "" or ...
401: 
402:           unless found or methods[methodname] or methodname == "initialize" then
403:             self.add_missing_method(klassname, orig_name)
404:           end
405: 
406:         else # not a test_.* method
407:           unless testmethodname =~ /^util_/ then
408:             puts "# WARNING Skipping #{testklassname}\##{testmethodname}" if $DEBUG
409:           end
410:         end # testmethodname =~ ...
411:       end # testmethods.each_key
412:     else # ! @klasses[klassname]
413:       puts "# ERROR class #{klassname} does not exist" if $DEBUG
414:       @error_count += 1
415: 
416:       @test_klasses[testklassname].keys.each do |testmethodname|
417:         @missing_methods[klassname][test_to_normal(testmethodname)] = true
418:       end
419:     end # @klasses[klassname]
420:   end
convert_class_name(name) click to toggle source

Generate the name of a testclass from non-test class so that Foo::Blah => TestFoo::TestBlah, etc. It the name is already a test class, convert it the other way.

     # File lib/zentest.rb, line 187
187:   def convert_class_name(name)
188:     name = name.to_s
189: 
190:     if self.is_test_class(name) then
191:       if $r then
192:         name = name.gsub(/Test($|::)/, '\1') # FooTest::BlahTest => Foo::Blah
193:       else
194:         name = name.gsub(/(^|::)Test/, '\1') # TestFoo::TestBlah => Foo::Blah
195:       end
196:     else
197:       if $r then
198:         name = name.gsub(/($|::)/, 'Test\1') # Foo::Blah => FooTest::BlahTest
199:       else
200:         name = name.gsub(/(^|::)/, '\1Test') # Foo::Blah => TestFoo::TestBlah
201:       end
202:     end
203: 
204:     return name
205:   end
create_method(indentunit, indent, name) click to toggle source

create a given method at a given indentation. Returns an array containing the lines of the method.

     # File lib/zentest.rb, line 439
439:   def create_method(indentunit, indent, name)
440:     meth = []
441:     meth.push indentunit*indent + "def #{name}"
442:     meth.last << "(*args)" unless name =~ /^test/
443:     indent += 1
444:     meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{name}'"
445:     indent -= 1
446:     meth.push indentunit*indent + "end"
447:     return meth
448:   end
generate_code() click to toggle source

Using the results gathered during analysis generate skeletal code with methods raising NotImplementedError, so that they can be filled in later, and so the tests will fail to start with.

     # File lib/zentest.rb, line 469
469:   def generate_code
470:     @result.unshift "# Code Generated by ZenTest v. #{VERSION}"
471: 
472:     if $DEBUG then
473:       @result.push "# found classes: #{@klasses.keys.join(', ')}"
474:       @result.push "# found test classes: #{@test_klasses.keys.join(', ')}"
475:     end
476: 
477:     if @missing_methods.size > 0 then
478:       @result.push ""
479:       @result.push "require 'test/unit/testcase'"
480:       @result.push "require 'test/unit' if $0 == __FILE__"
481:       @result.push ""
482:     end
483: 
484:     indentunit = "  "
485: 
486:     @missing_methods.keys.sort.each do |fullklasspath|
487: 
488:       methods = @missing_methods[fullklasspath]
489:       cls_methods = methods.keys.grep(/^(self\.|test_class_)/)
490:       methods.delete_if {|k,v| cls_methods.include? k }
491: 
492:       next if methods.empty? and cls_methods.empty?
493: 
494:       indent = 0
495:       is_test_class = self.is_test_class(fullklasspath)
496:       klasspath = fullklasspath.split(/::/)
497:       klassname = klasspath.pop
498: 
499:       klasspath.each do | modulename |
500:         m = self.get_class(modulename)
501:         type = m.nil? ? "module" : m.class.name.downcase
502:         @result.push indentunit*indent + "#{type} #{modulename}"
503:         indent += 1
504:       end
505:       @result.push indentunit*indent + "class #{klassname}" + (is_test_class ? " < Test::Unit::TestCase" : '')
506:       indent += 1
507: 
508:       meths = []
509: 
510:       cls_methods.sort.each do |method|
511:         meth = create_method(indentunit, indent, method)
512:         meths.push meth.join("\n")
513:       end
514: 
515:       methods.keys.sort.each do |method|
516:         next if method =~ /pretty_print/
517:         meth = create_method(indentunit, indent, method)
518:         meths.push meth.join("\n")
519:       end
520: 
521:       @result.push meths.join("\n\n")
522: 
523:       indent -= 1
524:       @result.push indentunit*indent + "end"
525:       klasspath.each do | modulename |
526:         indent -= 1
527:         @result.push indentunit*indent + "end"
528:       end
529:       @result.push ''
530:     end
531: 
532:     @result.push "# Number of errors detected: #{@error_count}"
533:     @result.push ''
534:   end
get_class(klassname) click to toggle source

obtain the class klassname, either from Module or using ObjectSpace to search for it.

     # File lib/zentest.rb, line 96
 96:   def get_class(klassname)
 97:     begin
 98:       klass = Module.const_get(klassname.intern)
 99:       puts "# found class #{klass.name}" if $DEBUG
100:     rescue NameError
101:       ObjectSpace.each_object(Class) do |cls|
102:         if cls.name =~ /(^|::)#{klassname}$/ then
103:           klass = cls
104:           klassname = cls.name
105:           break
106:         end
107:       end
108:       puts "# searched and found #{klass.name}" if klass and $DEBUG
109:     end
110: 
111:     if klass.nil? and not $TESTING then
112:       puts "Could not figure out how to get #{klassname}..."
113:       puts "Report to support-zentest@zenspider.com w/ relevant source"
114:     end
115: 
116:     return klass
117:   end
get_inherited_methods_for(klass, full) click to toggle source

Return the methods for class klass, as a hash with the method nemas as keys, and true as the value for all keys. Unless full is true, leave out the methods for Object which all classes get.

     # File lib/zentest.rb, line 152
152:   def get_inherited_methods_for(klass, full)
153:     klass = self.get_class(klass) if klass.kind_of? String
154: 
155:     klassmethods = {}
156:     if (klass.class.method_defined?(:superclass)) then
157:       superklass = klass.superclass
158:       if superklass then
159:         the_methods = superklass.instance_methods(true)
160: 
161:         # generally we don't test Object's methods...
162:         unless full then
163:           the_methods -= Object.instance_methods(true)
164:           the_methods -= Kernel.methods # FIX (true) - check 1.6 vs 1.8
165:         end
166: 
167:         the_methods.each do |meth|
168:           klassmethods[meth.to_s] = true
169:         end
170:       end
171:     end
172:     return klassmethods
173:   end
get_methods_for(klass, full=false) click to toggle source

Get the public instance, class and singleton methods for class klass. If full is true, include the methods from Kernel and other modules that get included. The methods suite, new, pretty_print, pretty_print_cycle will not be included in the resuting array.

     # File lib/zentest.rb, line 124
124:   def get_methods_for(klass, full=false)
125:     klass = self.get_class(klass) if klass.kind_of? String
126: 
127:     # WTF? public_instance_methods: default vs true vs false = 3 answers
128:     # to_s on all results if ruby >= 1.9
129:     public_methods = klass.public_instance_methods(false)
130:     public_methods -= Kernel.methods unless full
131:     public_methods.map! { |m| m.to_s }
132:     public_methods -= %(pretty_print pretty_print_cycle)
133: 
134:     klass_methods = klass.singleton_methods(full)
135:     klass_methods -= Class.public_methods(true)
136:     klass_methods = klass_methods.map { |m| "self.#{m}" }
137:     klass_methods  -= %(self.suite new)
138: 
139:     result = {}
140:     (public_methods + klass_methods).each do |meth|
141:       puts "# found method #{meth}" if $DEBUG
142:       result[meth] = true
143:     end
144: 
145:     return result
146:   end
is_test_class(klass) click to toggle source

Check the class klass is a testing class (by inspecting its name).

     # File lib/zentest.rb, line 177
177:   def is_test_class(klass)
178:     klass = klass.to_s
179:     klasspath = klass.split(/::/)
180:     a_bad_classpath = klasspath.find do |s| s !~ ($r ? /Test$/ : /^Test/) end
181:     return a_bad_classpath.nil?
182:   end
load_file(file) click to toggle source

load_file wraps require, skipping the loading of $0.

    # File lib/zentest.rb, line 80
80:   def load_file(file)
81:     puts "# loading #{file} // #{$0}" if $DEBUG
82: 
83:     unless file == $0 then
84:       begin
85:         require file
86:       rescue LoadError => err
87:         puts "Could not load #{file}: #{err}"
88:       end
89:     else
90:       puts "# Skipping loading myself (#{file})" if $DEBUG
91:     end
92:   end
methods_and_tests(klassname, testklassname) click to toggle source

looks up the methods and the corresponding test methods in the collection already built. To reduce duplication and hide implementation details.

     # File lib/zentest.rb, line 332
332:   def methods_and_tests(klassname, testklassname)
333:     return @klasses[klassname], @test_klasses[testklassname]
334:   end
missing_methods() click to toggle source
    # File lib/zentest.rb, line 66
66:     def missing_methods; raise "Something is wack"; end
process_class(klassname, full=false) click to toggle source

Does all the work of finding a class by name, obtaining its methods and those of its superclass. The full parameter determines if all the methods including those of Object and mixed in modules are obtained (true if they are, false by default).

     # File lib/zentest.rb, line 212
212:   def process_class(klassname, full=false)
213:     klass = self.get_class(klassname)
214:     raise "Couldn't get class for #{klassname}" if klass.nil?
215:     klassname = klass.name # refetch to get full name
216: 
217:     is_test_class = self.is_test_class(klassname)
218:     target = is_test_class ? @test_klasses : @klasses
219: 
220:     # record public instance methods JUST in this class
221:     target[klassname] = self.get_methods_for(klass, full)
222: 
223:     # record ALL instance methods including superclasses (minus Object)
224:     # Only minus Object if full is true.
225:     @inherited_methods[klassname] = self.get_inherited_methods_for(klass, full)
226:     return klassname
227:   end
result() click to toggle source

presents results in a readable manner.

     # File lib/zentest.rb, line 537
537:   def result
538:     return @result.join("\n")
539:   end
scan_files(*files) click to toggle source

Work through files, collecting class names, method names and assertions. Detects ZenTest (SKIP|FULL) comments in the bodies of classes. For each class a count of methods and test methods is kept, and the ratio noted.

     # File lib/zentest.rb, line 234
234:   def scan_files(*files)
235:     assert_count = Hash.new(0)
236:     method_count = Hash.new(0)
237:     klassname = nil
238: 
239:     files.each do |path|
240:       is_loaded = false
241: 
242:       # if reading stdin, slurp the whole thing at once
243:       file = (path == "-" ? $stdin.read : File.new(path))
244: 
245:       file.each_line do |line|
246: 
247:         if klassname then
248:           case line
249:           when /^\s*def/ then
250:             method_count[klassname] += 1
251:           when /assert|flunk/ then
252:             assert_count[klassname] += 1
253:           end
254:         end
255: 
256:         if line =~ /^\s*(?:class|module)\s+([\w:]+)/ then
257:           klassname = $1
258: 
259:           if line =~ /\#\s*ZenTest SKIP/ then
260:             klassname = nil
261:             next
262:           end
263: 
264:           full = false
265:           if line =~ /\#\s*ZenTest FULL/ then
266:             full = true
267:           end
268: 
269:           unless is_loaded then
270:             unless path == "-" then
271:               self.load_file(path)
272:             else
273:               eval file, TOPLEVEL_BINDING
274:             end
275:             is_loaded = true
276:           end
277: 
278:           begin
279:             klassname = self.process_class(klassname, full)
280:           rescue
281:             puts "# Couldn't find class for name #{klassname}"
282:             next
283:           end
284: 
285:           # Special Case: ZenTest is already loaded since we are running it
286:           if klassname == "TestZenTest" then
287:             klassname = "ZenTest"
288:             self.process_class(klassname, false)
289:           end
290: 
291:         end # if /class/
292:       end # IO.foreach
293:     end # files
294: 
295:     result = []
296:     method_count.each_key do |classname|
297: 
298:       entry = {}
299: 
300:       next if is_test_class(classname)
301:       testclassname = convert_class_name(classname)
302:       a_count = assert_count[testclassname]
303:       m_count = method_count[classname]
304:       ratio = a_count.to_f / m_count.to_f * 100.0
305: 
306:       entry['n'] = classname
307:       entry['r'] = ratio
308:       entry['a'] = a_count
309:       entry['m'] = m_count
310: 
311:       result.push entry
312:     end
313: 
314:     sorted_results = result.sort { |a,b| b['r'] <=> a['r'] }
315: 
316:     @result.push sprintf("# %25s: %4s / %4s = %6s%%", "classname", "asrt", "meth", "ratio")
317:     sorted_results.each do |e|
318:       @result.push sprintf("# %25s: %4d / %4d = %6.2f%%", e['n'], e['a'], e['m'], e['r'])
319:     end
320:   end
test_to_normal(_name, klassname=nil) click to toggle source
     # File lib/zentest.rb, line 422
422:   def test_to_normal(_name, klassname=nil)
423:     super do |name|
424:       if defined? @inherited_methods then
425:         known_methods = (@inherited_methods[klassname] || {}).keys.sort.reverse
426:         known_methods_re = known_methods.map {|s| Regexp.escape(s) }.join("|")
427: 
428:         name = name.sub(/^(#{known_methods_re})(_.*)?$/) { $1 } unless
429:           known_methods_re.empty?
430: 
431:         name
432:       end
433:     end
434:   end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.