require "drb/drb" require "thread" require "fileutils" require "autospec/reload_css" require "autospec/base_runner" require "autospec/simple_runner" require "autospec/spork_runner" module Autospec; end class Autospec::Runner MATCHERS = {} def self.watch(pattern, &blk) MATCHERS[pattern] = blk end watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } # Rails example watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" } watch(%r{^spec/support/(.+)\.rb$}) { "spec" } watch("app/controllers/application_controller.rb") { "spec/controllers" } # Capybara request specs watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } # Fabrication watch(%r{^spec/fabricators/(.+)_fabricator\.rb$}) { "spec" } RELOAD_MATCHERS = Set.new def self.watch_reload(pattern) RELOAD_MATCHERS << pattern end watch_reload('spec/spec_helper.rb') watch_reload('config/(.*).rb') watch_reload(%r{app/helpers/(.*).rb}) def self.run(opts={}) self.new.run(opts) end def initialize @queue = [] @mutex = Mutex.new @signal = ConditionVariable.new start_service_queue end def run(opts = {}) puts "Forced polling (slower) - inotify does not work on network filesystems, use local filesystem to avoid" if opts[:force_polling] if ENV["SPORK"] == "0" puts "Using Simple Runner" @runner = Autospec::SimpleRunner.new else puts "Using Spork Runner" @runner = Autospec::SporkRunner.new end @runner.start Signal.trap("HUP") {@runner.stop; exit } Signal.trap("SIGINT") {@runner.stop; exit } options = {filter: /^app|^spec|^lib/, relative_paths: true} if opts[:force_polling] options[:force_polling] = true options[:latency] = opts[:latency] || 3 end Thread.start do Listen.to('.', options ) do |modified, added, removed| process_change([modified, added].flatten.compact) end end @mutex.synchronize do @queue << ['spec', 'spec'] @signal.signal end while @runner.running? process_queue end rescue => e puts e puts e.backtrace @runner.stop end def process_queue STDIN.gets if @queue.length == 0 @queue << ['spec', 'spec'] @signal.signal else specs = failed_specs(:delete => false) puts puts if specs.length == 0 puts "No specs have failed yet!" puts else puts "The following specs have failed: " specs.each do |s| puts s end puts queue_specs(specs.zip specs) end end end def wait_for(timeout_milliseconds) timeout = (timeout_milliseconds + 0.0) / 1000 finish = Time.now + timeout t = Thread.new do while Time.now < finish && !yield sleep(0.001) end end t.join rescue nil end def force_polling? works = false begin require 'rb-inotify' require 'fileutils' n = INotify::Notifier.new FileUtils.touch('./tmp/test_polling') n.watch("./tmp", :modify, :attrib){ works = true } quit = false Thread.new do while !works && !quit if IO.select([n.to_io], [], [], 0.1) n.process end end end sleep 0.01 FileUtils.touch('./tmp/test_polling') wait_for(100) { works } File.unlink('./tmp/test_polling') n.stop quit = true rescue LoadError #assume it works (mac) works = true end !works end def process_change(files) return unless files.length > 0 specs = [] hit = false files.each do |file| RELOAD_MATCHERS.each do |k| if k.match(file) @runner.reload return end end MATCHERS.each do |k,v| if m = k.match(file) hit = true spec = v ? ( v.arity == 1 ? v.call(m) : v.call ) : file if File.exists?(spec) || Dir.exists?(spec) specs << [file, spec] end end end Autospec::ReloadCss::MATCHERS.each do |k,v| matches = [] if k.match(file) matches << file end Autospec::ReloadCss.run_on_change(matches) if matches.present? end end queue_specs(specs) if hit rescue => e p "failed in watcher" p e p e.backtrace end def queue_specs(specs) if specs.length == 0 locked = @mutex.try_lock if locked @signal.signal @mutex.unlock end return else @runner.abort end @mutex.synchronize do specs.each do |c,spec| @queue.delete([c,spec]) if @queue.last && @queue.last[0] == "focus" focus = @queue.pop @queue << [c,spec] if focus[1].include?(spec) || c != spec @queue << focus end else @queue << [c,spec] end end @signal.signal end end def thread_loop @mutex.synchronize do last_failed = false current = @queue.last if current last_failed = process_spec(current[1]) end wait = @queue.length == 0 || last_failed @signal.wait(@mutex) if wait end rescue => e p "DISASTA PASTA" puts e puts e.backtrace end def process_spec(spec) last_failed = false result = run_spec(spec) if result == 0 @queue.pop else last_failed = true if result.to_i > 0 focus_on_failed_tests ensure_all_specs_will_run end end last_failed end def start_service_queue @worker ||= Thread.new do while true thread_loop end end end def focus_on_failed_tests current = @queue.last specs = failed_specs[0..10] if current[0] == "focus" @queue.pop end @queue << ["focus", specs.join(" ")] end def ensure_all_specs_will_run unless @queue.any?{|s,t| t == 'spec'} @queue.unshift(['spec','spec']) end end def failed_specs(opts={:delete => true}) specs = [] path = './tmp/rspec_result' if File.exist?(path) specs = File.open(path) { |file| file.read.split("\n") } File.delete(path) if opts[:delete] end specs end def run_spec(specs) File.delete("tmp/rspec_result") if File.exists?("tmp/rspec_result") args = ["-f", "progress", specs.split(" "), "-r", "#{File.dirname(__FILE__)}/formatter.rb", "-f", "Autospec::Formatter"].flatten @runner.run(args, specs) end end