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