class ZenTest

def self.autotest(*klasses)

environment.
and eval it to put the methods into the runtime
and analyse the results. Generate the skeletal code
Process all the supplied classes for methods etc,
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

def self.fix(*files)

If no files are supplied, splutter out some help.
skeleton code written.
they are analysed and the missing methods have
Runs ZenTest over all the supplied files so that
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

def self.usage

Provide a certain amount of help.
def self.usage
  puts <<-EO_USAGE
age: #{File.basename $0} [options] test-and-implementation-files...
nTest scans your target and unit-test code and writes your missing
de based on simple naming rules, enabling XP at a much quicker
ce. ZenTest only works with Ruby and Minitest or Test::Unit.
nTest uses the following rules to figure out what code should be
nerated:
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.
tions:
-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).
  EO_USAGE
end

def self.usage_with_exit

Give help, then quit.
def self.usage_with_exit
  self.usage
  exit 0
end

def add_missing_method(klassname, methodname)

Adds a missing method to the collected results.
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

def analyze

Then do it in the other direction...
a test method
Walk each known class and test that each method has
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

def analyze_impl(klassname)

added to the information for that class
has a corrsponding test method. If it doesn't this is
Checks, for the given class klassname, that each method
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

def analyze_test(testklassname)

If not, add them to the information about that class.
the test methods have corresponding (normal) methods.
For the given test class testklassname, ensure that all
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

def convert_class_name(name)

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

def create_method(indentunit, indent, name)

the lines of the method.
indentation. Returns an array containing
create a given method at a given
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

def generate_code

in later, and so the tests will fail to start with.
NotImplementedError, so that they can be filled
generate skeletal code with methods raising
Using the results gathered during analysis
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

def get_class(klassname)

obtain the class klassname
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

def get_inherited_methods_for(klass, full)

all classes get.
Unless full is true, leave out the methods for Object which
method nemas as keys, and true as the value for all keys.
Return the methods for class klass, as a hash with the
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

def get_methods_for(klass, full=false)

be included in the resuting array.
suite, new, pretty_print, pretty_print_cycle will not
Kernel and other modules that get included. The methods
class klass. If full is true, include the methods from
Get the public instance, class and singleton methods for
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 -= %w(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  -= %w(self.suite new)
  result = {}
  (public_methods + klass_methods).each do |meth|
    puts "# found method #{meth}" if $DEBUG
    result[meth] = true
  end
  return result
end

def initialize

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

def is_test_class(klass)

(by inspecting its name).
Check the class klass is a testing class
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

def load_file(file)

load_file wraps require, skipping the loading of $0.
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

def methods_and_tests(klassname, testklassname)

and hide implementation details.
in the collection already built. To reduce duplication
looks up the methods and the corresponding test methods
def methods_and_tests(klassname, testklassname)
  return @klasses[klassname], @test_klasses[testklassname]
end

def missing_methods; raise "Something is wack"; end

def missing_methods; raise "Something is wack"; end

def process_class(klassname, full=false)

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

def result

presents results in a readable manner.
def result
  return @result.join("\n")
end

def scan_files(*files)

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