From ef875bba1265ee534bd39f9a9b27ccf71ca3321f Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 25 Apr 2013 17:17:43 +1000 Subject: [PATCH] a spec that works like the fsm intended --- lib/autospec/formatter.rb | 20 ++++ lib/autospec/runner.rb | 232 ++++++++++++++++++++++++++++++++++++++ lib/tasks/autospec.rake | 9 ++ 3 files changed, 261 insertions(+) create mode 100644 lib/autospec/formatter.rb create mode 100644 lib/autospec/runner.rb create mode 100644 lib/tasks/autospec.rake diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb new file mode 100644 index 00000000000..bbc151b194c --- /dev/null +++ b/lib/autospec/formatter.rb @@ -0,0 +1,20 @@ +require "rspec/core/formatters/base_formatter" + +module Autospec; end +class Autospec::Formatter < RSpec::Core::Formatters::BaseFormatter + + def dump_summary(duration, total, failures, pending) + failed_specs = examples.delete_if{|e| e.execution_result[:status] != "failed"}.map{|s| s.metadata[:location]} + + # if this fails don't kill everything + begin + FileUtils.mkdir_p('tmp') + File.open("./tmp/rspec_result","w") do |f| + f.puts failed_specs.join("\n") + end + rescue + # nothing really we can do, at least don't kill the test runner + end + end + +end diff --git a/lib/autospec/runner.rb b/lib/autospec/runner.rb new file mode 100644 index 00000000000..5cef584a245 --- /dev/null +++ b/lib/autospec/runner.rb @@ -0,0 +1,232 @@ +require "drb/drb" +require "thread" + +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" } + watch('spec/spec_helper.rb') { "spec" } + + # 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" } + + + def self.run + self.new.run + end + + def initialize + @queue = [] + @mutex = Mutex.new + @signal = ConditionVariable.new + start_service_queue + end + + def run + if already_running?(pid_file) + puts "autospec appears to be running, it is possible the pid file is old" + puts "if you are sure it is not running, delete #{pid_file}" + return + end + write_pid_file(pid_file, Process.pid) + + start_spork + Signal.trap("HUP") {stop_spork; exit } + Signal.trap("SIGINT") {stop_spork; exit } + + Thread.start do + Listen.to('.', relative_paths: true) do |modified, added, removed| + process_change([modified, added].flatten.compact) + end + end + + @mutex.synchronize do + @queue << ['spec', 'spec'] + @signal.signal + end + + Process.wait + + rescue => e + puts e + puts e.backtrace + stop_spork + end + + + def process_change(files) + return unless files.length > 0 + specs = [] + files.each do |file| + MATCHERS.each do |k,v| + if m = k.match(file) + spec = v ? ( v.arity == 1 ? v.call(m) : v.call ) : file + if File.exists?(spec) || Dir.exists?(spec) + specs << [file, spec] + end + end + end + end + queue_specs(specs) + 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 + spork_service.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 start_service_queue + @worker ||= Thread.new do + while true + @mutex.synchronize do + last_failed = false + current = @queue.last + if current + result = run_spec(current[1]) + if result == 0 + @queue.pop + else + last_failed = true + if result.to_i > 0 + # focus + specs = failed_specs[0..10] + if current[0] == "focus" + @queue.pop + end + @queue << ["focus", specs.join(" ")] + end + end + end + @signal.wait(@mutex) if @queue.length == 0 || last_failed + end + end + end + end + + def failed_specs + specs = [] + path = './tmp/rspec_result' + if File.exist?(path) + specs = File.open(path) { |file| file.read.split("\n") } + File.delete(path) + 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 + + spork_service.run(args,$stderr,$stdout) + end + + + def spork_pid_file + Rails.root + "tmp/pids/spork.pid" + end + + def pid_file + Rails.root + "tmp/pids/autospec.pid" + end + + def already_running?(pid_file) + if File.exists? pid_file + pid = File.read(pid_file).to_i + Process.getpgid(pid) rescue nil + end + end + + def write_pid_file(file,pid) + FileUtils.mkdir_p(Rails.root + "tmp/pids") + File.open(file,'w') do |f| + f.write(pid) + end + end + + def spork_running? + spork_service.port rescue nil + end + + def spork_service + + unless @drb_listener_running + begin + DRb.start_service("druby://127.0.0.1:0") + rescue SocketError, Errno::EADDRNOTAVAIL + DRb.start_service("druby://:0") + end + + @drb_listener_running = true + end + + @spork_service ||= DRbObject.new_with_uri("druby://127.0.0.1:8989") + end + + def stop_spork + pid = File.read(spork_pid_file).to_i + Process.kill("SIGHUP",pid) + end + + def start_spork + + if already_running?(spork_pid_file) + puts "Killing old orphan spork instance" + stop_spork + sleep 1 + end + + @spork_pid = Process.spawn("RAILS_ENV=test bundle exec spork") + write_pid_file(spork_pid_file, @spork_pid) + + running = false + while !running + running = spork_running? + sleep 0.1 + end + + end +end diff --git a/lib/tasks/autospec.rake b/lib/tasks/autospec.rake new file mode 100644 index 00000000000..20c5db253f1 --- /dev/null +++ b/lib/tasks/autospec.rake @@ -0,0 +1,9 @@ +# I like guard, don't get me wrong, but it is just not working right +# architectually it can not do what I want it to do, this is how I want +# it to behave + +desc "Run all specs automatically as needed" +task "autospec" => :environment do + require 'autospec/runner' + Autospec::Runner.run +end