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 Minitest or 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 579
579:   def self.autotest(*klasses)
580:     zentest = ZenTest.new
581:     klasses.each do |klass|
582:       zentest.process_class(klass)
583:     end
584: 
585:     zentest.analyze
586: 
587:     zentest.missing_methods.each do |klass,methods|
588:       methods.each do |method,x|
589:         warn "autotest generating #{klass}##{method}"
590:       end
591:     end
592: 
593:     zentest.generate_code
594:     code = zentest.result
595:     puts code if $DEBUG
596: 
597:     Object.class_eval code
598:   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. If no files are supplied, splutter out some help.

     # File lib/zentest.rb, line 566
566:   def self.fix(*files)
567:     ZenTest.usage_with_exit if files.empty?
568:     zentest = ZenTest.new
569:     zentest.scan_files(*files)
570:     zentest.analyze
571:     zentest.generate_code
572:     return zentest.result
573:   end
new() click to toggle source
    # File lib/zentest.rb, line 74
74:   def initialize
75:     @result = []
76:     @test_klasses = {}
77:     @klasses = {}
78:     @error_count = 0
79:     @inherited_methods = Hash.new { |h,k| h[k] = {} }
80:     # key = klassname, val = hash of methods => true
81:     @missing_methods = Hash.new { |h,k| h[k] = {} }
82:   end
usage() click to toggle source

Provide a certain amount of help.

     # File lib/zentest.rb, line 523
523:   def self.usage
524:     puts usage: #{File.basename $0} [options] test-and-implementation-files...ZenTest scans your target and unit-test code and writes your missingcode based on simple naming rules, enabling XP at a much quickerpace. ZenTest only works with Ruby and Minitest or Test::Unit.ZenTest uses the following rules to figure out what code should begenerated:* Definition:  * CUT = Class Under Test  * TC = Test Class (for CUT)* TC's name is the same as CUT w/ "Test" prepended at every scope level.  * Example: TestA::TestB vs A::B.* CUT method names are used in CT, with "test_" prependend and optional "_ext" extensions for differentiating test case edge boundaries.  * Example:    * A::B#blah    * TestA::TestB#test_blah_normal    * TestA::TestB#test_blah_missing_file* All naming conventions are bidirectional with the exception of test extensions.options:  -h display this information  -v display version information  -r Reverse mapping (ClassTest instead of TestClass)  -e (Rapid XP) eval the code generated instead of printing it  -t test/unit generation (default is minitest).
525:   end
usage_with_exit() click to toggle source

Give help, then quit.

     # File lib/zentest.rb, line 557
557:   def self.usage_with_exit
558:     self.usage
559:     exit 0
560:   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 328
328:   def add_missing_method(klassname, methodname)
329:     @result.push "# ERROR method #{klassname}\##{methodname} does not exist (1)" if $DEBUG and not $TESTING
330:     @error_count += 1
331:     @missing_methods[klassname][methodname] = true
332:   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 444
444:   def analyze
445:     # walk each known class and test that each method has a test method
446:     @klasses.each_key do |klassname|
447:       self.analyze_impl(klassname)
448:     end
449: 
450:     # now do it in the other direction...
451:     @test_klasses.each_key do |testklassname|
452:       self.analyze_test(testklassname)
453:     end
454:   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 344
344:   def analyze_impl(klassname)
345:     testklassname = self.convert_class_name(klassname)
346:     if @test_klasses[testklassname] then
347:       _, testmethods = methods_and_tests(klassname, testklassname)
348: 
349:       # check that each method has a test method
350:       @klasses[klassname].each_key do | methodname |
351:         testmethodname = normal_to_test(methodname)
352:         unless testmethods[testmethodname] then
353:           begin
354:             unless testmethods.keys.find { |m| m =~ /#{testmethodname}(_\w+)+$/ } then
355:               self.add_missing_method(testklassname, testmethodname)
356:             end
357:           rescue RegexpError
358:             puts "# ERROR trying to use '#{testmethodname}' as a regex. Look at #{klassname}.#{methodname}"
359:           end
360:         end # testmethods[testmethodname]
361:       end # @klasses[klassname].each_key
362:     else # ! @test_klasses[testklassname]
363:       puts "# ERROR test class #{testklassname} does not exist" if $DEBUG
364:       @error_count += 1
365: 
366:       @klasses[klassname].keys.each do | methodname |
367:         self.add_missing_method(testklassname, normal_to_test(methodname))
368:       end
369:     end # @test_klasses[testklassname]
370:   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 375
375:   def analyze_test(testklassname)
376:     klassname = self.convert_class_name(testklassname)
377: 
378:     # CUT might be against a core class, if so, slurp it and analyze it
379:     if $stdlib[klassname] then
380:       self.process_class(klassname, true)
381:       self.analyze_impl(klassname)
382:     end
383: 
384:     if @klasses[klassname] then
385:       methods, testmethods = methods_and_tests(klassname,testklassname)
386: 
387:       # check that each test method has a method
388:       testmethods.each_key do | testmethodname |
389:         if testmethodname =~ /^test_(?!integration_)/ then
390: 
391:           # try the current name
392:           methodname = test_to_normal(testmethodname, klassname)
393:           orig_name = methodname.dup
394: 
395:           found = false
396:           until methodname == "" or methods[methodname] or @inherited_methods[klassname][methodname] do
397:               # try the name minus an option (ie mut_opt1 -> mut)
398:             if methodname.sub!(/_[^_]+$/, '') then
399:               if methods[methodname] or @inherited_methods[klassname][methodname] then
400:                 found = true
401:               end
402:             else
403:               break # no more substitutions will take place
404:             end
405:           end # methodname == "" or ...
406: 
407:           unless found or methods[methodname] or methodname == "initialize" then
408:             self.add_missing_method(klassname, orig_name)
409:           end
410: 
411:         else # not a test_.* method
412:           unless testmethodname =~ /^util_/ then
413:             puts "# WARNING Skipping #{testklassname}\##{testmethodname}" if $DEBUG
414:           end
415:         end # testmethodname =~ ...
416:       end # testmethods.each_key
417:     else # ! @klasses[klassname]
418:       puts "# ERROR class #{klassname} does not exist" if $DEBUG
419:       @error_count += 1
420: 
421:       @test_klasses[testklassname].keys.each do |testmethodname|
422:         @missing_methods[klassname][test_to_normal(testmethodname)] = true
423:       end
424:     end # @klasses[klassname]
425:   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 192
192:   def convert_class_name(name)
193:     name = name.to_s
194: 
195:     if self.is_test_class(name) then
196:       if $r then
197:         name = name.gsub(/Test($|::)/, '\1') # FooTest::BlahTest => Foo::Blah
198:       else
199:         name = name.gsub(/(^|::)Test/, '\1') # TestFoo::TestBlah => Foo::Blah
200:       end
201:     else
202:       if $r then
203:         name = name.gsub(/($|::)/, 'Test\1') # Foo::Blah => FooTest::BlahTest
204:       else
205:         name = name.gsub(/(^|::)/, '\1Test') # Foo::Blah => TestFoo::TestBlah
206:       end
207:     end
208: 
209:     return name
210:   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 430
430:   def create_method(indentunit, indent, name)
431:     meth = []
432:     meth.push indentunit*indent + "def #{name}"
433:     meth.last << "(*args)" unless name =~ /^test/
434:     indent += 1
435:     meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{name}'"
436:     indent -= 1
437:     meth.push indentunit*indent + "end"
438:     return meth
439:   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 460
460:   def generate_code
461:     @result.unshift "# Code Generated by ZenTest v. #{VERSION}"
462: 
463:     if $DEBUG then
464:       @result.push "# found classes: #{@klasses.keys.join(', ')}"
465:       @result.push "# found test classes: #{@test_klasses.keys.join(', ')}"
466:     end
467: 
468:     if @missing_methods.size > 0 then
469:       @result.push ""
470:       @result.push "require 'test/unit/testcase'"
471:       @result.push "require 'test/unit' if $0 == __FILE__"
472:       @result.push ""
473:     end
474: 
475:     indentunit = "  "
476: 
477:     @missing_methods.keys.sort.each do |fullklasspath|
478:       methods = @missing_methods[fullklasspath]
479:       cls_methods = methods.keys.grep(/^(self\.|test_class_)/)
480:       methods.delete_if {|k,v| cls_methods.include? k }
481: 
482:       next if methods.empty? and cls_methods.empty?
483: 
484:       indent = 0
485:       is_test_class = self.is_test_class(fullklasspath)
486: 
487:       clsname = $t ? "Test::Unit::TestCase" : "MiniTest::Unit::TestCase"
488:       superclass = is_test_class ? " < #{clsname}" : ''
489: 
490:       @result.push indentunit*indent + "class #{fullklasspath}#{superclass}"
491:       indent += 1
492: 
493:       meths = []
494: 
495:       cls_methods.sort.each do |method|
496:         meth = create_method(indentunit, indent, method)
497:         meths.push meth.join("\n")
498:       end
499: 
500:       methods.keys.sort.each do |method|
501:         next if method =~ /pretty_print/
502:         meth = create_method(indentunit, indent, method)
503:         meths.push meth.join("\n")
504:       end
505: 
506:       @result.push meths.join("\n\n")
507: 
508:       indent -= 1
509:       @result.push indentunit*indent + "end"
510:       @result.push ''
511:     end
512: 
513:     @result.push "# Number of errors detected: #{@error_count}"
514:     @result.push ''
515:   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 101
101:   def get_class(klassname)
102:     begin
103:       klass = klassname.split(/::/).inject(Object) { |k,n| k.const_get n }
104:       puts "# found class #{klass.name}" if $DEBUG
105:     rescue NameError
106:       ObjectSpace.each_object(Class) do |cls|
107:         if cls.name =~ /(^|::)#{klassname}$/ then
108:           klass = cls
109:           klassname = cls.name
110:           break
111:         end
112:       end
113:       puts "# searched and found #{klass.name}" if klass and $DEBUG
114:     end
115: 
116:     if klass.nil? and not $TESTING then
117:       puts "Could not figure out how to get #{klassname}..."
118:       puts "Report to support-zentest@zenspider.com w/ relevant source"
119:     end
120: 
121:     return klass
122:   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 157
157:   def get_inherited_methods_for(klass, full)
158:     klass = self.get_class(klass) if klass.kind_of? String
159: 
160:     klassmethods = {}
161:     if (klass.class.method_defined?(:superclass)) then
162:       superklass = klass.superclass
163:       if superklass then
164:         the_methods = superklass.instance_methods(true)
165: 
166:         # generally we don't test Object's methods...
167:         unless full then
168:           the_methods -= Object.instance_methods(true)
169:           the_methods -= Kernel.methods # FIX (true) - check 1.6 vs 1.8
170:         end
171: 
172:         the_methods.each do |meth|
173:           klassmethods[meth.to_s] = true
174:         end
175:       end
176:     end
177:     return klassmethods
178:   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 129
129:   def get_methods_for(klass, full=false)
130:     klass = self.get_class(klass) if klass.kind_of? String
131: 
132:     # WTF? public_instance_methods: default vs true vs false = 3 answers
133:     # to_s on all results if ruby >= 1.9
134:     public_methods = klass.public_instance_methods(false)
135:     public_methods -= Kernel.methods unless full
136:     public_methods.map! { |m| m.to_s }
137:     public_methods -= %(pretty_print pretty_print_cycle)
138: 
139:     klass_methods = klass.singleton_methods(full)
140:     klass_methods -= Class.public_methods(true)
141:     klass_methods = klass_methods.map { |m| "self.#{m}" }
142:     klass_methods  -= %(self.suite new)
143: 
144:     result = {}
145:     (public_methods + klass_methods).each do |meth|
146:       puts "# found method #{meth}" if $DEBUG
147:       result[meth] = true
148:     end
149: 
150:     return result
151:   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 182
182:   def is_test_class(klass)
183:     klass = klass.to_s
184:     klasspath = klass.split(/::/)
185:     a_bad_classpath = klasspath.find do |s| s !~ ($r ? /Test$/ : /^Test/) end
186:     return a_bad_classpath.nil?
187:   end
load_file(file) click to toggle source

load_file wraps require, skipping the loading of $0.

    # File lib/zentest.rb, line 85
85:   def load_file(file)
86:     puts "# loading #{file} // #{$0}" if $DEBUG
87: 
88:     unless file == $0 then
89:       begin
90:         require file
91:       rescue LoadError => err
92:         puts "Could not load #{file}: #{err}"
93:       end
94:     else
95:       puts "# Skipping loading myself (#{file})" if $DEBUG
96:     end
97:   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 337
337:   def methods_and_tests(klassname, testklassname)
338:     return @klasses[klassname], @test_klasses[testklassname]
339:   end
missing_methods() click to toggle source
    # File lib/zentest.rb, line 71
71:     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 217
217:   def process_class(klassname, full=false)
218:     klass = self.get_class(klassname)
219:     raise "Couldn't get class for #{klassname}" if klass.nil?
220:     klassname = klass.name # refetch to get full name
221: 
222:     is_test_class = self.is_test_class(klassname)
223:     target = is_test_class ? @test_klasses : @klasses
224: 
225:     # record public instance methods JUST in this class
226:     target[klassname] = self.get_methods_for(klass, full)
227: 
228:     # record ALL instance methods including superclasses (minus Object)
229:     # Only minus Object if full is true.
230:     @inherited_methods[klassname] = self.get_inherited_methods_for(klass, full)
231:     return klassname
232:   end
result() click to toggle source

presents results in a readable manner.

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

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.