Class: ZenTest

### Home Classes Methods

### In Files

- lib/zentest.rb

### Parent

Object

### Methods

- ::autotest
- ::fix
- ::new
- ::usage
- ::usage_with_exit
- #add_missing_method
- #analyze
- #analyze_impl
- #analyze_test
- #convert_class_name
- #create_method
- #generate_code
- #get_class
- #get_inherited_methods_for
- #get_methods_for
- #is_test_class
- #load_file
- #methods_and_tests
- #missing_methods
- #process_class
- #result
- #scan_files

### Included Modules

- ZenTestMapping

### Files

- History.txt
- Manifest.txt
- README.txt
- how_to_use_zentest.txt
- example.txt

### Class/Module Index

Quicksearch

- Autotest
- Autotest::AutoUpdate
- Autotest::Bundler
- Autotest::Isolate
- Autotest::Once
- Autotest::Preload
- Autotest::RCov
- Autotest::Restart
- Autotest::Timestamp
- FunctionalTestMatrix
- Module
- Multiruby
- UnitDiff
- ZenTest
- ZenTestMapping

No matching classes.

# 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:

- 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.

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 581
    def self.autotest(*klasses)
      zentest = ZenTest.new
      klasses.each do |klass|
        zentest.process_class(klass)
      end

      zentest.analyze

      zentest.missing_methods.each do |klass,methods|
        methods.each do |method,x|
          warn "autotest generating #{klass}##{method}"
        end
      end

      zentest.generate_code
      code = zentest.result
      puts code if $DEBUG

      Object.class_eval code
    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 568
    def self.fix(*files)
      ZenTest.usage_with_exit if files.empty?
      zentest = ZenTest.new
      zentest.scan_files(*files)
      zentest.analyze
      zentest.generate_code
      return zentest.result
    end

new() click to toggle source

    # File lib/zentest.rb, line 80
    def initialize
      @result = []
      @test_klasses = {}
      @klasses = {}
      @error_count = 0
      @inherited_methods = Hash.new { |h,k| h[k] = {} }
      # key = klassname, val = hash of methods => true
      @missing_methods = Hash.new { |h,k| h[k] = {} }
    end

usage() click to toggle source

Provide a certain amount of help.

    # File lib/zentest.rb, line 525
    def self.usage
      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).
    end

usage_with_exit() click to toggle source

Give help, then quit.

    # File lib/zentest.rb, line 559
    def self.usage_with_exit
      self.usage
      exit 0
    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 326
    def add_missing_method(klassname, methodname)
      @result.push "# ERROR method #{klassname}\##{methodname} does not exist (1)" if $DEBUG and not $TESTING
      @error_count += 1
      @missing_methods[klassname][methodname] = true
    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 442
    def analyze
      # walk each known class and test that each method has a test method
      @klasses.each_key do |klassname|
        self.analyze_impl(klassname)
      end

      # now do it in the other direction...
      @test_klasses.each_key do |testklassname|
        self.analyze_test(testklassname)
      end
    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 342
    def analyze_impl(klassname)
      testklassname = self.convert_class_name(klassname)
      if @test_klasses[testklassname] then
        _, testmethods = methods_and_tests(klassname, testklassname)

        # check that each method has a test method
        @klasses[klassname].each_key do | methodname |
          testmethodname = normal_to_test(methodname)
          unless testmethods[testmethodname] then
            begin
              unless testmethods.keys.find { |m| m =~ /#{testmethodname}(_\w+)+$/ } then
                self.add_missing_method(testklassname, testmethodname)
              end
            rescue RegexpError
              puts "# ERROR trying to use '#{testmethodname}' as a regex. Look at #{klassname}.#{methodname}"
            end
          end # testmethods[testmethodname]
        end # @klasses[klassname].each_key
      else # ! @test_klasses[testklassname]
        puts "# ERROR test class #{testklassname} does not exist" if $DEBUG
        @error_count += 1

        @klasses[klassname].keys.each do | methodname |
          self.add_missing_method(testklassname, normal_to_test(methodname))
        end
      end # @test_klasses[testklassname]
    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 373
    def analyze_test(testklassname)
      klassname = self.convert_class_name(testklassname)

      # CUT might be against a core class, if so, slurp it and analyze it
      if $stdlib[klassname] then
        self.process_class(klassname, true)
        self.analyze_impl(klassname)
      end

      if @klasses[klassname] then
        methods, testmethods = methods_and_tests(klassname,testklassname)

        # check that each test method has a method
        testmethods.each_key do | testmethodname |
          if testmethodname =~ /^test_(?!integration_)/ then

            # try the current name
            methodname = test_to_normal(testmethodname, klassname)
            orig_name = methodname.dup

            found = false
            until methodname == "" or methods[methodname] or @inherited_methods[klassname][methodname] do
                # try the name minus an option (ie mut_opt1 -> mut)
              if methodname.sub!(/_[^_]+$/, '') then
                if methods[methodname] or @inherited_methods[klassname][methodname] then
                  found = true
                end
              else
                break # no more substitutions will take place
              end
            end # methodname == "" or ...

            unless found or methods[methodname] or methodname == "initialize" then
              self.add_missing_method(klassname, orig_name)
            end

          else # not a test_.* method
            unless testmethodname =~ /^util_/ then
              puts "# WARNING Skipping #{testklassname}\##{testmethodname}" if $DEBUG
            end
          end # testmethodname =~ ...
        end # testmethods.each_key
      else # ! @klasses[klassname]
        puts "# ERROR class #{klassname} does not exist" if $DEBUG
        @error_count += 1

        @test_klasses[testklassname].keys.each do |testmethodname|
          @missing_methods[klassname][test_to_normal(testmethodname)] = true
        end
      end # @klasses[klassname]
    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 190
    def convert_class_name(name)
      name = name.to_s

      if self.is_test_class(name) then
        if $r then
          name = name.gsub(/Test($|::)/, '\1') # FooTest::BlahTest => Foo::Blah
        else
          name = name.gsub(/(^|::)Test/, '\1') # TestFoo::TestBlah => Foo::Blah
        end
      else
        if $r then
          name = name.gsub(/($|::)/, 'Test\1') # Foo::Blah => FooTest::BlahTest
        else
          name = name.gsub(/(^|::)/, '\1Test') # Foo::Blah => TestFoo::TestBlah
        end
      end

      return name
    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 428
    def create_method(indentunit, indent, name)
      meth = []
      meth.push indentunit*indent + "def #{name}"
      meth.last << "(*args)" unless name =~ /^test/
      indent += 1
      meth.push indentunit*indent + "raise NotImplementedError, 'Need to write #{name}'"
      indent -= 1
      meth.push indentunit*indent + "end"
      return meth
    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 458
    def generate_code
      @result.unshift "# Code Generated by ZenTest v. #{VERSION}"

      if $DEBUG then
        @result.push "# found classes: #{@klasses.keys.join(', ')}"
        @result.push "# found test classes: #{@test_klasses.keys.join(', ')}"
      end

      if @missing_methods.size > 0 then
        @result.push ""
        if $t then
          @result.push "require 'test/unit/testcase'"
          @result.push "require 'test/unit' if $0 == __FILE__"
        else
          @result.push "require 'minitest/autorun'"
        end
        @result.push ""
      end

      indentunit = "  "

      @missing_methods.keys.sort.each do |fullklasspath|
        methods = @missing_methods[fullklasspath]
        cls_methods = methods.keys.grep(/^(self\.|test_class_)/)
        methods.delete_if {|k,v| cls_methods.include? k }

        next if methods.empty? and cls_methods.empty?

        indent = 0
        is_test_class = self.is_test_class(fullklasspath)

        clsname = $t ? "Test::Unit::TestCase" : "Minitest::Test"
        superclass = is_test_class ? " < #{clsname}" : ''

        @result.push indentunit*indent + "class #{fullklasspath}#{superclass}"
        indent += 1

        meths = []

        cls_methods.sort.each do |method|
          meth = create_method(indentunit, indent, method)
          meths.push meth.join("\n")
        end

        methods.keys.sort.each do |method|
          next if method =~ /pretty_print/
          meth = create_method(indentunit, indent, method)
          meths.push meth.join("\n")
        end

        @result.push meths.join("\n\n")

        indent -= 1
        @result.push indentunit*indent + "end"
        @result.push ''
      end

      @result.push "# Number of errors detected: #{@error_count}"
      @result.push ''
    end

get_class(klassname) click to toggle source

obtain the class klassname

    # File lib/zentest.rb, line 106
    def get_class(klassname)
      klass = nil
      begin
        klass = klassname.split(/::/).inject(Object) { |k,n| k.const_get n }
        puts "# found class #{klass.name}" if $DEBUG
      rescue NameError
      end

      if klass.nil? and not $TESTING then
        puts "Could not figure out how to get #{klassname}..."
        puts "Report to support-zentest@zenspider.com w/ relevant source"
      end

      return klass
    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 155
    def get_inherited_methods_for(klass, full)
      klass = self.get_class(klass) if klass.kind_of? String

      klassmethods = {}
      if (klass.class.method_defined?(:superclass)) then
        superklass = klass.superclass
        if superklass then
          the_methods = superklass.instance_methods(true)

          # generally we don't test Object's methods...
          unless full then
            the_methods -= Object.instance_methods(true)
            the_methods -= Kernel.methods # FIX (true) - check 1.6 vs 1.8
          end

          the_methods.each do |meth|
            klassmethods[meth.to_s] = true
          end
        end
      end
      return klassmethods
    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 127
    def get_methods_for(klass, full=false)
      klass = self.get_class(klass) if klass.kind_of? String

      # WTF? public_instance_methods: default vs true vs false = 3 answers
      # to_s on all results if ruby >= 1.9
      public_methods = klass.public_instance_methods(false)
      public_methods -= Kernel.methods unless full
      public_methods.map! { |m| m.to_s }
      public_methods -= %(pretty_print pretty_print_cycle)

      klass_methods = klass.singleton_methods(full)
      klass_methods -= Class.public_methods(true)
      klass_methods = klass_methods.map { |m| "self.#{m}" }
      klass_methods  -= %(self.suite new)

      result = {}
      (public_methods + klass_methods).each do |meth|
        puts "# found method #{meth}" if $DEBUG
        result[meth] = true
      end

      return result
    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 180
    def is_test_class(klass)
      klass = klass.to_s
      klasspath = klass.split(/::/)
      a_bad_classpath = klasspath.find do |s| s !~ ($r ? /Test$/ : /^Test/) end
      return a_bad_classpath.nil?
    end

load_file(file) click to toggle source

load_file wraps require, skipping the loading of $0.

    # File lib/zentest.rb, line 91
    def load_file(file)
      puts "# loading #{file} // #{$0}" if $DEBUG

      unless file == $0 then
        begin
          require file
        rescue LoadError => err
          puts "Could not load #{file}: #{err}"
        end
      else
        puts "# Skipping loading myself (#{file})" if $DEBUG
      end
    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 335
    def methods_and_tests(klassname, testklassname)
      return @klasses[klassname], @test_klasses[testklassname]
    end

missing_methods() click to toggle source

    # File lib/zentest.rb, line 77
    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 215
    def process_class(klassname, full=false)
      klass = self.get_class(klassname)
      raise "Couldn't get class for #{klassname}" if klass.nil?
      klassname = klass.name # refetch to get full name

      is_test_class = self.is_test_class(klassname)
      target = is_test_class ? @test_klasses : @klasses

      # record public instance methods JUST in this class
      target[klassname] = self.get_methods_for(klass, full)

      # record ALL instance methods including superclasses (minus Object)
      # Only minus Object if full is true.
      @inherited_methods[klassname] = self.get_inherited_methods_for(klass, full)
      return klassname
    end

result() click to toggle source

presents results in a readable manner.

    # File lib/zentest.rb, line 520
    def result
      return @result.join("\n")
    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 237
    def scan_files(*files)
      assert_count = Hash.new(0)
      method_count = Hash.new(0)
      klassname = nil

      files.each do |path|
        is_loaded = false

        # if reading stdin, slurp the whole thing at once
        file = (path == "-" ? $stdin.read : File.new(path))

        file.each_line do |line|

          if klassname then
            case line
            when /^\s*def/ then
              method_count[klassname] += 1
            when /assert|flunk/ then
              assert_count[klassname] += 1
            end
          end

          if line =~ /^\s*(?:class|module)\s+([\w:]+)/ then
            klassname = $1

            if line =~ /\#\s*ZenTest SKIP/ then
              klassname = nil
              next
            end

            full = false
            if line =~ /\#\s*ZenTest FULL/ then
              full = true
            end

            unless is_loaded then
              unless path == "-" then
                self.load_file(path)
              else
                eval file, TOPLEVEL_BINDING
              end
              is_loaded = true
            end

            begin
              klassname = self.process_class(klassname, full)
            rescue
              puts "# Couldn't find class for name #{klassname}"
              next
            end

            # Special Case: ZenTest is already loaded since we are running it
            if klassname == "TestZenTest" then
              klassname = "ZenTest"
              self.process_class(klassname, false)
            end

          end # if /class/
        end # IO.foreach
      end # files

      result = []
      method_count.each_key do |classname|

        entry = {}

        next if is_test_class(classname)
        testclassname = convert_class_name(classname)
        a_count = assert_count[testclassname]
        m_count = method_count[classname]
        ratio = a_count.to_f / m_count.to_f * 100.0

        entry['n'] = classname
        entry['r'] = ratio
        entry['a'] = a_count
        entry['m'] = m_count

        result.push entry
      end

      sorted_results = result.sort { |a,b| b['r'] <=> a['r'] }

      @result.push sprintf("# %25s: %4s / %4s = %6s%%", "classname", "asrt", "meth", "ratio")
      sorted_results.each do |e|
        @result.push sprintf("# %25s: %4d / %4d = %6.2f%%", e['n'], e['a'], e['m'], e['r'])
      end
    end

[Validate]

Generated with the Darkfish Rdoc Generator 2.