class Autotest

def self.add_discovery &proc

def self.add_discovery &proc
  @@discoveries << proc
end

def self.add_hook(name, &block)

def self.add_hook(name, &block)
  HOOKS[name] << block
end

def self.autodiscover


end
"rails" if File.exist? 'config/environment.rb'
Autotest.add_discovery do

=== Example autotest/discover.rb:

4. Invoke run method on appropriate class (eg Autotest::RailsRspec.run).
3. Require file by sorting styles and joining (eg 'autotest/rails_rspec').
2. Those procs determine your styles (eg ["rails", "rspec"]).
1. All autotest/discover.rb files loaded.

=== Process:

a corresponding name.
environment. That plugin should define a subclass of Autotest with
combined to dynamically invoke an autotest plugin to suite your
describing the user's current environment. Those styles are then
+add_discovery+. That proc should return one or more strings
should register discovery procs with autotest using
"autotest/discover.rb". If found, that file is loaded and it
searching your loadpath, vendor/plugins, and rubygems for
Automatically find all potential autotest runner styles by
#
def self.autodiscover
  require 'rubygems'
  begin
    require 'win32console' if WINDOZE
  rescue LoadError
  end
  with_current_path_in_load_path do
    # search load paths for autotest/discover.rb and load em all
    Gem.find_files("autotest/discover").each do |f|
      load f
    end
  end
  #call all discover procs an determine style
  @@discoveries.map{ |proc| proc.call }.flatten.compact.sort.uniq
end

def self.options;@@options;end

def self.options;@@options;end

def self.rubygem_load_paths

list of all available rubygem load paths
def self.rubygem_load_paths
  begin
    require 'rubygems'
    Gem.latest_load_paths
  rescue LoadError
    []
  end
end

def self.run

def self.run
  new.run
end

def self.with_current_path_in_load_path

since ruby 1.9 current path (path where autotest was called from) is not in $LOAD_PATH
def self.with_current_path_in_load_path
  if RUBY19 and not $LOAD_PATH.include?(File.expand_path('.')) and not $LOAD_PATH.include?('.')
    begin
      $LOAD_PATH << '.'
      result = yield
    ensure
      $LOAD_PATH.delete('.')
    end
    result
  else
    yield
  end
end

def add_exception regexp

def add_exception regexp
  raise "exceptions already compiled" if defined? @exceptions
  @exception_list << regexp
  nil
end

def add_mapping(regexp, prepend = false, &proc)

def add_mapping(regexp, prepend = false, &proc)
  if prepend then
    @test_mappings.unshift [regexp, proc]
  else
    @test_mappings.push [regexp, proc]
  end
  nil
end

def add_sigint_handler

def add_sigint_handler
  trap 'INT' do
    if self.interrupted then
      self.wants_to_quit = true
    else
      unless hook :interrupt then
        puts "Interrupt a second time to quit"
        self.interrupted = true
        Kernel.sleep 1.5
      end
      raise Interrupt, nil # let the run loop catch it
    end
  end
end

def add_test_unit_mappings

def add_test_unit_mappings
  #file in /lib -> run test in /test
  self.add_mapping(/^lib\/.*\.rb$/) do |filename, _|
    possible = File.basename(filename).gsub '_', '_?'
    files_matching %r%^test/.*#{possible}$%
  end
  #file in /test -> run it
  self.add_mapping(/^test.*\/test_.*rb$/) do |filename, _|
    filename
  end
end

def all_good

def all_good
  files_to_test.empty?
end

def bundle_exec

def bundle_exec
  options[:bundle_exec] ? 'bundle exec ' : ''
end

def clear_exceptions

def clear_exceptions
  raise "exceptions already compiled" if defined? @exceptions
  @exception_list.clear
  nil
end

def clear_mappings

def clear_mappings
  @test_mappings.clear
  nil
end

def consolidate_failures(failed)

def consolidate_failures(failed)
  filters = new_hash_of_arrays
  class_map = Hash[*self.find_order.grep(/^test/).map { |f| # TODO: ugly
                     [path_to_classname(f), f]
                   }.flatten]
  class_map.merge!(self.extra_class_map)
  failed.each do |method, klass|
    if class_map.has_key? klass then
      filters[class_map[klass]] << method
    else
      output.puts "Unable to map class #{klass} to a file"
    end
  end
  return filters
end

def escape_filenames(classes)

def escape_filenames(classes)
  classes.map{|klass| "'#{klass}'"}
end

def exceptions

def exceptions
  unless defined? @exceptions then
    if @exception_list.empty? then
      @exceptions = nil
    else
      @exceptions = Regexp.union(*@exception_list)
    end
  end
  @exceptions
end

def files_matching regexp

def files_matching regexp
  self.find_order.select { |k| k =~ regexp }
end

def find_files

def find_files
  result = {}
  targets = self.find_directories + self.extra_files
  self.find_order.clear
  targets.each do |target|
    order = []
    Find.find(target) do |f|
      Find.prune if f =~ self.exceptions
      next if test ?d, f
      next if f =~ /(swp|~|rej|orig)$/ # temporary/patch files
      next if f =~ /\/\.?#/            # Emacs autosave/cvs merge files
      filename = f.sub(/^\.\//, '')
      result[filename] = File.stat(filename).mtime rescue next
      order << filename
    end
    self.find_order.push(*order.sort)
  end
  return result
end

def find_files_to_test(files=find_files)

modified.
the latest mtime of the files modified or nil when nothing was
timestamps, and use this to update the files to test. Returns
Find the files which have been modified, update the recorded
#
def find_files_to_test(files=find_files)
  updated = files.select { |filename, mtime| self.last_mtime < mtime }
  unless updated.empty? or self.last_mtime.to_i == 0 #nothing to update or initial run
    p updated if options[:verbose]
    hook :updated, updated
  end
  updated.map { |f,m| test_files_for(f) }.flatten.uniq.each do |filename|
    self.files_to_test[filename] # creates key with default value
  end
  if updated.empty? then
    nil
  else
    files.values.max
  end
end

def get_to_green

def get_to_green
  begin
    run_tests
    wait_for_changes unless all_good
  end until all_good
end

def handle_results(results)

def handle_results(results)
  failed = results.scan(self.failed_results_re)
  completed = results =~ self.completed_re
  self.files_to_test = consolidate_failures failed if completed
  color = completed && self.files_to_test.empty? ? :green : :red
  hook color unless $TESTING
  self.tainted = true unless self.files_to_test.empty?
end

def hook(name, *args)

def hook(name, *args)
  deprecated = {
    # none currently
  }
  if deprecated[name] and not HOOKS[name].empty? then
    warn "hook #{name} has been deprecated, use #{deprecated[name]}"
  end
  HOOKS[name].any? do |plugin|
    plugin[self, *args]
  end
end

def initialize

def initialize
  # these two are set directly because they're wrapped with
  # add/remove/clear accessor methods
  @exception_list = []
  @test_mappings = []
  self.completed_re = /\d+ tests, \d+ assertions, \d+ failures, \d+ errors/
  self.extra_class_map   = {}
  self.extra_files       = []
  self.failed_results_re = /^\s+\d+\) (?:Failure|Error):\n(.*?)\((.*?)\)/
  self.files_to_test     = new_hash_of_arrays
  self.find_order        = []
  self.known_files       = nil
  self.libs              = %w[. lib test].join(File::PATH_SEPARATOR)
  self.order             = :random
  self.output            = $stderr
  self.sleep             = 1
  self.testlib           = "test/unit"
  self.find_directories  = ['.']
  self.unit_diff         = "#{File.expand_path("#{File.dirname(__FILE__)}/../bin/unit_diff")} -u"
  add_test_unit_mappings
  #execute custom extensions
  load_custom_extensions(options[:rc])
end

def known_files

def known_files
  unless @known_files then
    @known_files = Hash[*find_order.map { |f| [f, true] }.flatten]
  end
  @known_files
end

def load_custom_extensions(config_file)

def load_custom_extensions(config_file)
  configs = ['./.autotest']
  if config_file
    configs << File.expand_path(config_file)
  else
    configs << File.expand_path('~/.autotest')
  end
  configs.each do |f|
    load f if File.exist? f
  end
end

def make_test_cmd files_to_test

def make_test_cmd files_to_test
  cmds = []
  full, partial = reorder(files_to_test).partition { |k,v| v.empty? }
  base_cmd = "#{bundle_exec}#{ruby} -I#{libs} -rubygems"
  unless full.empty? then
    files = full.map {|k,v| k}.flatten.uniq
    if options[:parallel] and files.size > 1
      files = files.map{|file| File.expand_path(file) } if RUBY19
      cmds << "#{bundle_exec}parallel_test #{escape_filenames(files).join(' ')}"
    else
      files.unshift testlib
      cmds << "#{base_cmd} -e \"[#{escape_filenames(files).join(', ')}].each { |f| require f }\" | #{unit_diff}"
    end
  end
  partial.each do |klass, methods|
    regexp = Regexp.union(*methods).source
    cmds << "#{base_cmd} #{klass} -n \"/^(#{regexp})$/\" | #{unit_diff}"
  end
  cmds.join("#{SEP} ")
end

def new_hash_of_arrays

def new_hash_of_arrays
  Hash.new { |h,k| h[k] = [] }
end

def options;@@options;end

def options;@@options;end

def path_to_classname(s)

def path_to_classname(s)
  sep = File::SEPARATOR
  f = s.sub(/^test#{sep}/, '').sub(/\.rb$/, '').split(sep)
  f = f.map { |path| path.split(/_|(\d+)/).map { |seg| seg.capitalize }.join }
  f = f.map { |path| path =~ /^Test/ ? path : "Test#{path}"  }
  f.join('::')
end

def remove_exception regexp

def remove_exception regexp
  raise "exceptions already compiled" if defined? @exceptions
  @exception_list.delete regexp
  nil
end

def remove_mapping regexp

def remove_mapping regexp
  @test_mappings.delete_if do |k,v|
    k == regexp
  end
  nil
end

def reorder files_to_test

def reorder files_to_test
  case self.order
  when :alpha then
    files_to_test.sort_by { |k,v| k }
  when :reverse then
    files_to_test.sort_by { |k,v| k }.reverse
  when :random then
    max = files_to_test.size
    files_to_test.sort_by { |k,v| rand(max) }
  when :natural then
    (self.find_order & files_to_test.keys).map { |f| [f, files_to_test[f]] }
  else
    raise "unknown order type: #{self.order.inspect}"
  end
end

def rerun_all_tests

def rerun_all_tests
  reset
  run_tests
  hook :all_good if all_good
end

def reset

def reset
  self.files_to_test.clear
  self.find_order.clear
  self.interrupted = false
  self.known_files = nil
  self.last_mtime = T0
  self.tainted = false
  self.wants_to_quit = false
  hook :reset
end

def ruby

def ruby
  ruby = ENV['RUBY']
  ruby ||= File.join(Config::CONFIG['bindir'],
                     Config::CONFIG['ruby_install_name'])
  ruby.gsub! File::SEPARATOR, File::ALT_SEPARATOR if File::ALT_SEPARATOR
  return ruby
end

def run

def run
  hook :initialize
  reset
  add_sigint_handler
  self.last_mtime = Time.now if options[:no_full_after_start]
  loop do
    begin # ^c handler
      get_to_green
      if tainted? and not options[:no_full_after_failed]
        rerun_all_tests
      else
        hook :all_good
      end
      wait_for_changes
    rescue Interrupt
      break if wants_to_quit
      reset
    end
  end
  hook :quit
rescue Exception => err
  hook :died, err
end

def run_tests

def run_tests
  hook :run_command
  new_mtime = self.find_files_to_test
  return unless new_mtime
  self.last_mtime = new_mtime
  cmd = self.make_test_cmd self.files_to_test
  return if cmd.empty?
  puts cmd unless options[:quiet]
  old_sync = $stdout.sync
  $stdout.sync = true
  self.results = []
  line = []
  begin
    open("| #{cmd}", "r") do |f|
      until f.eof? do
        c = f.getc or break
        putc (c.is_a?(Fixnum) ? c.chr : c) # print breaks coloring on windows -> putc
        line << c
        if c == ?\n then
          self.results << if RUBY19 then
                            line.join
                          else
                            line.pack "c*"
                          end
          line.clear
        end
      end
    end
  ensure
    $stdout.sync = old_sync
  end
  hook :ran_command
  self.results = self.results.join
  handle_results(self.results)
end

def test_files_for(filename)

def test_files_for(filename)
  result = @test_mappings.find { |file_re, ignored| filename =~ file_re }
  p :test_file_for => [filename, result.first] if result and $DEBUG
  result = result.nil? ? [] : [result.last.call(filename, $~)].flatten
  output.puts "No tests matched #{filename}" if
    (options[:verbose] or $TESTING) and result.empty?
  result.sort.uniq.select { |f| known_files[f] }
end

def wait_for_changes

def wait_for_changes
  hook :waiting
  Kernel.sleep self.sleep until find_files_to_test
end