Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge guillaumegentil's rspactor fork

  • Loading branch information...
commit ce0fcc90fc9b281aa11604f8591dadf53819dd17 1 parent fcddab1
rubyphunk authored
View
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Mislav Marohnić
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
58 Rakefile
@@ -0,0 +1,58 @@
+task :default => :spec
+
+desc "starts RSpactor"
+task :spec do
+ system "ruby -Ilib bin/rspactor"
+end
+
+desc "generates .gemspec file"
+task :gemspec => "version:read" do
+ spec = Gem::Specification.new do |gem|
+ gem.name = "rspactor"
+ gem.summary = "RSpactor is a command line tool to automatically run your changed specs & cucumber features (much like autotest)."
+ gem.description = "read summary!"
+ gem.email = "guillaumegentil@gmail.com"
+ gem.homepage = "http://github.com/guillaumegentil/rspactor"
+ gem.authors = ["Mislav Marohnić", "Andreas Wolff", "Pelle Braendgaard", "Thibaud Guillaume-Gentil"]
+ gem.has_rdoc = false
+
+ gem.version = GEM_VERSION
+ gem.files = FileList['Rakefile', '{bin,lib,images,spec}/**/*', 'README*', 'LICENSE*']
+ gem.executables = Dir['bin/*'].map { |f| File.basename(f) }
+ end
+
+ spec_string = spec.to_ruby
+
+ begin
+ Thread.new { eval("$SAFE = 3\n#{spec_string}", binding) }.join
+ rescue
+ abort "unsafe gemspec: #{$!}"
+ else
+ File.open("#{spec.name}.gemspec", 'w') { |file| file.write spec_string }
+ end
+end
+
+task :bump => ["version:bump", :gemspec]
+
+namespace :version do
+ task :read do
+ unless defined? GEM_VERSION
+ GEM_VERSION = File.read("VERSION")
+ end
+ end
+
+ task :bump => :read do
+ if ENV['VERSION']
+ GEM_VERSION.replace ENV['VERSION']
+ else
+ GEM_VERSION.sub!(/\d+$/) { |num| num.to_i + 1 }
+ end
+
+ File.open("VERSION", 'w') { |v| v.write GEM_VERSION }
+ end
+end
+
+task :release => :bump do
+ system %(git commit VERSION *.gemspec -m "release v#{GEM_VERSION}")
+ system %(git tag -am "release v#{GEM_VERSION}" v#{GEM_VERSION})
+end
View
1  VERSION
@@ -0,0 +1 @@
+0.5.3
View
11 bin/rspactor
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+require 'rspactor/runner'
+
+RSpactor::Runner.start({
+ :coral => ARGV.delete('--coral'),
+ :celerity => ARGV.delete('--celerity'),
+ :spork => ARGV.delete('--drb'),
+ :view => ARGV.delete('--view'), # by default, rspactor didn't catch specs view
+ :clear => ARGV.delete('--clear'),
+ :run_in => ARGV.last
+})
View
BIN  images/failed.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  images/pending.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  images/success.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
61 lib/cucumber_growler.rb
@@ -0,0 +1,61 @@
+require 'cucumber'
+require 'cucumber/formatter/console'
+require File.dirname(__FILE__) + '/rspactor/growl'
+
+module CucumberGrowler
+ include RSpactor::Growl
+
+ def self.included(base)
+ base.class_eval do
+ alias original_print_stats print_stats
+ include InstanceMethods
+
+ def print_stats(features)
+ title, icon, messages = '', '', []
+ [:failed, :skipped, :undefined, :pending, :passed].reverse.each do |status|
+ if step_mother.steps(status).any?
+ icon = icon_for(status)
+ # title = title_for(status)
+ messages << dump_count(step_mother.steps(status).length, "step", status.to_s)
+ end
+ end
+
+ notify "Cucumber Results", messages.reverse.join(", "), icon
+ original_print_stats(features)
+ end
+ end
+ end
+
+ module InstanceMethods
+ def icon_for(status)
+ case status
+ when :passed
+ 'success'
+ when :pending, :undefined, :skipped
+ 'pending'
+ when :failed
+ 'failed'
+ end
+ end
+
+ def title_for(status)
+ case status
+ when :passed
+ 'Features passed!'
+ when :pending
+ 'Some steps are pending...'
+ when :undefined
+ 'Some undefined steps...'
+ when :skipped
+ 'Some steps skipped...'
+ when :failed
+ 'Failures occurred!'
+ end
+ end
+ end
+
+end
+
+module Cucumber::Formatter::Console
+ include CucumberGrowler
+end
View
9 lib/rspactor.rb
@@ -0,0 +1,9 @@
+module RSpactor
+ autoload :Interactor, 'rspactor/interactor'
+ autoload :Listener, 'rspactor/listener'
+ autoload :Inspector, 'rspactor/inspector'
+ autoload :Runner, 'rspactor/runner'
+ autoload :Growl, 'rspactor/growl'
+ autoload :Spork, 'rspactor/spork'
+ autoload :Celerity, 'rspactor/celerity'
+end
View
29 lib/rspactor/celerity.rb
@@ -0,0 +1,29 @@
+require 'rspactor'
+
+module RSpactor
+ class Celerity
+
+ def self.start(dir)
+ pid_path = "#{dir}/tmp/pids/mongrel_celerity.pid"
+ if File.exist?(pid_path)
+ system("kill $(head #{pid_path}) >/dev/null 2>&1")
+ system("rm #{pid_path} >/dev/null 2>&1")
+ end
+ # kill other mongrels
+ system("kill $(ps aux | grep 'mongrel_rails' | grep -v grep | awk '//{print $2;}') >/dev/null 2>&1")
+ system("rake celerity_server:start >/dev/null 2>&1 &")
+ Interactor.ticker_msg "** Starting celerity server"
+ end
+
+ def self.restart
+ system("rake celerity_server:stop >/dev/null 2>&1 && rake celerity_server:start >/dev/null 2>&1 &")
+ Interactor.ticker_msg "** Restarting celerity server"
+ end
+
+ def self.kill_jruby
+ system("kill $(ps aux | grep jruby | grep -v grep | awk '//{print $2;}') >/dev/null 2>&1")
+ true
+ end
+
+ end
+end
View
14 lib/rspactor/growl.rb
@@ -0,0 +1,14 @@
+module RSpactor
+ module Growl
+ extend self
+
+ def notify(title, msg, icon, pri = 0)
+ system("growlnotify -w -n rspactor --image #{image_path(icon)} -p #{pri} -m #{msg.inspect} #{title} &")
+ end
+
+ # failed | pending | success
+ def image_path(icon)
+ File.expand_path File.dirname(__FILE__) + "/../../images/#{icon}.png"
+ end
+ end
+end
View
110 lib/rspactor/inspector.rb
@@ -0,0 +1,110 @@
+require 'rspactor'
+
+module RSpactor
+ # Maps the changed filenames to list of specs to run in the next go.
+ # Assumes Rails-like directory structure
+ class Inspector
+ EXTENSIONS = %w(rb erb builder haml rhtml rxml yml conf opts feature)
+
+ attr_reader :runner, :root
+
+ def initialize(runner)
+ @runner = runner
+ @root = runner.dir
+ end
+
+ def determine_files(file)
+ candidates = translate(file)
+ cucumberable = candidates.delete('cucumber')
+ candidates.reject { |candidate| candidate.index('.') }.each do |dir|
+ candidates.reject! { |candidate| candidate.index("#{dir}/") == 0 }
+ end
+ files = candidates.select { |candidate| File.exists? candidate }
+
+ if files.empty? && !candidates.empty? && !cucumberable
+ $stderr.puts "doesn't exist: #{candidates.inspect}"
+ end
+
+ files << 'cucumber' if cucumberable
+ files
+ end
+
+ # mappings for Rails are inspired by autotest mappings in rspec-rails
+ def translate(file)
+ file = file.sub(%r:^#{Regexp.escape(root)}/:, '')
+ candidates = []
+
+ if spec_file?(file)
+ candidates << file
+ elsif cucumber_file?(file)
+ candidates << 'cucumber'
+ else
+ spec_file = append_spec_file_extension(file)
+
+ case file
+ when %r:^app/:
+ if file =~ %r:^app/controllers/application(_controller)?.rb$:
+ candidates << 'controllers'
+ elsif file == 'app/helpers/application_helper.rb'
+ candidates << 'helpers' << 'views'
+ elsif !file.include?("app/views/") || runner.options[:view]
+ candidates << spec_file.sub('app/', '')
+
+ if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
+ candidates << append_spec_file_extension($1)
+ elsif file =~ %r:app/helpers/(\w+)_helper.rb:
+ candidates << "views/#{$1}"
+ elsif file =~ /_observer.rb$/
+ candidates << candidates.last.sub('_observer', '')
+ end
+ end
+ when %r:^lib/:
+ candidates << spec_file
+ # lib/foo/bar_spec.rb -> lib/bar_spec.rb
+ candidates << candidates.last.sub($&, '')
+ # lib/bar_spec.rb -> bar_spec.rb
+ candidates << candidates.last.sub(%r:\w+/:, '') if candidates.last.index('/')
+ when 'config/routes.rb'
+ candidates << 'controllers' << 'helpers' << 'views' << 'routing'
+ when 'config/database.yml', 'db/schema.rb', 'spec/factories.rb'
+ candidates << 'models'
+ when 'config/boot.rb', 'config/environment.rb', %r:^config/environments/:, %r:^config/initializers/:, %r:^vendor/:, 'spec/spec_helper.rb'
+ Spork.reload if runner.options[:spork]
+ Celerity.restart if runner.options[:celerity]
+ candidates << 'spec'
+ when %r:^config/:
+ # nothing
+ when %r:^(spec/(spec_helper|shared/.*)|config/(boot|environment(s/test)?))\.rb$:, 'spec/spec.opts', 'spec/fakeweb.rb'
+ candidates << 'spec'
+ else
+ candidates << spec_file
+ end
+ end
+
+ candidates.map do |candidate|
+ if candidate == 'cucumber'
+ candidate
+ elsif candidate.index('spec') == 0
+ File.join(root, candidate)
+ else
+ File.join(root, 'spec', candidate)
+ end
+ end
+ end
+
+ def append_spec_file_extension(file)
+ if File.extname(file) == ".rb"
+ file.sub(/.rb$/, "_spec.rb")
+ else
+ file + "_spec.rb"
+ end
+ end
+
+ def spec_file?(file)
+ file =~ /^spec\/.+_spec.rb$/
+ end
+ def cucumber_file?(file)
+ file =~ /^features\/.+$/
+ end
+ end
+end
View
85 lib/rspactor/interactor.rb
@@ -0,0 +1,85 @@
+require 'timeout'
+
+module RSpactor
+ class Interactor
+
+ attr_reader :runner
+
+ def initialize(runner)
+ @runner = runner
+ ticker
+ end
+
+ def self.ticker_msg(msg, seconds_to_wait = 3)
+ $stdout.print msg
+ seconds_to_wait.times do
+ $stdout.print('.')
+ $stdout.flush
+ sleep 1
+ end
+ $stdout.puts "\n"
+ end
+
+ def wait_for_enter_key(msg, seconds_to_wait, clear = runner.options[:clear])
+ begin
+ Timeout::timeout(seconds_to_wait) do
+ system("clear;") if clear
+ ticker(:start => true, :msg => msg)
+ $stdin.gets
+ return true
+ end
+ rescue Timeout::Error
+ false
+ ensure
+ ticker(:stop => true)
+ end
+ end
+
+ def start_termination_handler
+ @main_thread = Thread.current
+ Thread.new do
+ loop do
+ sleep 0.5
+ if entry = $stdin.gets
+ case entry
+ when "c\n" # Cucumber: current tagged feature
+ runner.run_cucumber_command
+ when "ca\n" # Cucumber All: ~pending tagged feature
+ runner.run_cucumber_command('~@wip,~@pending')
+ else
+ if wait_for_enter_key("** Running all specs... Hit <enter> again to exit RSpactor", 1)
+ @main_thread.exit
+ exit
+ end
+ runner.run_all_specs
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def ticker(opts = {})
+ if opts[:stop]
+ $stdout.puts "\n"
+ @pointer_running = false
+ elsif opts[:start]
+ @pointer_running = true
+ write(opts[:msg]) if opts[:msg]
+ else
+ Thread.new do
+ loop do
+ write('.') if @pointer_running == true
+ sleep 1.0
+ end
+ end
+ end
+ end
+
+ def write(msg)
+ $stdout.print(msg)
+ $stdout.flush
+ end
+ end
+end
View
88 lib/rspactor/listener.rb
@@ -0,0 +1,88 @@
+require 'osx/foundation'
+OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
+
+module RSpactor
+ # based on http://rails.aizatto.com/2007/11/28/taming-the-autotest-beast-with-fsevents/
+ class Listener
+ attr_reader :last_check, :callback, :valid_extensions
+
+ def initialize(valid_extensions = nil)
+ @valid_extensions = valid_extensions
+ timestamp_checked
+
+ @callback = lambda do |stream, ctx, num_events, paths, marks, event_ids|
+ changed_files = extract_changed_files_from_paths(split_paths(paths, num_events))
+ timestamp_checked
+ yield changed_files unless changed_files.empty?
+ end
+ end
+
+ def run(directories)
+ dirs = Array(directories)
+ stream = OSX::FSEventStreamCreate(OSX::KCFAllocatorDefault, callback, nil, dirs, OSX::KFSEventStreamEventIdSinceNow, 0.5, 0)
+ unless stream
+ $stderr.puts "Failed to create stream"
+ exit(1)
+ end
+
+ OSX::FSEventStreamScheduleWithRunLoop(stream, OSX::CFRunLoopGetCurrent(), OSX::KCFRunLoopDefaultMode)
+ unless OSX::FSEventStreamStart(stream)
+ $stderr.puts "Failed to start stream"
+ exit(1)
+ end
+
+ begin
+ OSX::CFRunLoopRun()
+ rescue Interrupt
+ OSX::FSEventStreamStop(stream)
+ OSX::FSEventStreamInvalidate(stream)
+ OSX::FSEventStreamRelease(stream)
+ end
+ end
+
+ def timestamp_checked
+ @last_check = Time.now
+ end
+
+ def split_paths(paths, num_events)
+ paths.regard_as('*')
+ rpaths = []
+ num_events.times { |i| rpaths << paths[i] }
+ rpaths
+ end
+
+ def extract_changed_files_from_paths(paths)
+ changed_files = []
+ paths.each do |path|
+ next if ignore_path?(path)
+ Dir.glob(path + "*").each do |file|
+ next if ignore_file?(file)
+ changed_files << file if file_changed?(file)
+ end
+ end
+ changed_files
+ end
+
+ def file_changed?(file)
+ File.stat(file).mtime > last_check
+ rescue Errno::ENOENT
+ false
+ end
+
+ def ignore_path?(path)
+ path =~ /(?:^|\/)\.(git|svn)/
+ end
+
+ def ignore_file?(file)
+ File.basename(file).index('.') == 0 or not valid_extension?(file)
+ end
+
+ def file_extension(file)
+ file =~ /\.(\w+)$/ and $1
+ end
+
+ def valid_extension?(file)
+ valid_extensions.nil? or valid_extensions.include?(file_extension(file))
+ end
+ end
+end
View
191 lib/rspactor/runner.rb
@@ -0,0 +1,191 @@
+require 'rspactor'
+
+module RSpactor
+ class Runner
+ def self.start(options = {})
+ run_in = options.delete(:run_in) || Dir.pwd
+ new(run_in, options).start
+ end
+
+ attr_reader :dir, :options, :inspector, :interactor
+
+ def initialize(dir, options = {})
+ @dir = dir
+ @options = options
+ read_git_head
+ end
+
+ def start
+ load_dotfile
+ puts "** RSpactor is now watching at '#{dir}'"
+ Spork.start if options[:spork]
+ Celerity.start(dir) if options[:celerity]
+ start_interactor
+ start_listener
+ end
+
+ def start_interactor
+ @interactor = Interactor.new(self)
+ aborted = @interactor.wait_for_enter_key("** Hit <enter> to skip initial spec & cucumber run", 2, false)
+ @interactor.start_termination_handler
+ unless aborted
+ run_all_specs
+ run_cucumber_command('~@wip,~@pending', false)
+ end
+ end
+
+ def start_listener
+ @inspector = Inspector.new(self)
+
+ Listener.new(Inspector::EXTENSIONS) do |files|
+ changed_files(files) unless git_head_changed?
+ end.run(dir)
+ end
+
+ def load_dotfile
+ dotfile = File.join(ENV['HOME'], '.rspactor')
+ if File.exists?(dotfile)
+ begin
+ Kernel.load dotfile
+ rescue => e
+ $stderr.puts "Error while loading #{dotfile}: #{e}"
+ end
+ end
+ end
+
+ def run_all_specs
+ run_spec_command(File.join(dir, 'spec'))
+ end
+
+ def run_spec_command(paths)
+ paths = Array(paths)
+ if paths.empty?
+ @last_run_failed = nil
+ else
+ cmd = [ruby_opts, spec_runner, paths, spec_opts].flatten.join(' ')
+ @last_run_failed = run_command(cmd)
+ end
+ end
+
+ def run_cucumber_command(tags = '@wip:2', clear = @options[:clear])
+ system("clear;") if clear
+ puts "** Running all #{tags} tagged features..."
+ cmd = [ruby_opts, cucumber_runner, cucumber_opts(tags)].flatten.join(' ')
+ @last_run_failed = run_command(cmd)
+ # Workaround for killing jruby process when used with celerity and spork
+ Celerity.kill_jruby if options[:celerity] && options[:spork]
+ end
+
+ def last_run_failed?
+ @last_run_failed == false
+ end
+
+ protected
+
+ def run_command(cmd)
+ system(cmd)
+ $?.success?
+ end
+
+ def changed_files(files)
+ files = files.inject([]) do |all, file|
+ all.concat inspector.determine_files(file)
+ end
+ unless files.empty?
+
+ # cucumber features
+ if files.delete('cucumber')
+ run_cucumber_command
+ end
+
+ # specs files
+ unless files.empty?
+ system("clear;") if @options[:clear]
+ files.uniq!
+ puts files.map { |f| f.to_s.gsub(/#{dir}/, '') }.join("\n")
+
+ previous_run_failed = last_run_failed?
+ run_spec_command(files)
+
+ if options[:retry_failed] and previous_run_failed and not last_run_failed?
+ run_all_specs
+ end
+ end
+ end
+ end
+
+ private
+
+ def spec_opts
+ if File.exist?('spec/spec.opts')
+ opts = File.read('spec/spec.opts').gsub("\n", ' ')
+ else
+ opts = "--color"
+ end
+
+ opts << spec_formatter_opts
+ # only add the "progress" formatter unless no other (besides growl) is specified
+ opts << ' -f progress' unless opts.scan(/\s(?:-f|--format)\b/).length > 1
+
+ opts
+ end
+
+ def cucumber_opts(tags)
+ if File.exist?('features/support/cucumber.opts')
+ opts = File.read('features/support/cucumber.opts').gsub("\n", ' ')
+ else
+ opts = "--color --format progress --drb --no-profile"
+ end
+
+ opts << " --tags #{tags}"
+ opts << cucumber_formatter_opts
+ opts << " --require features" # because using require option overwrite default require
+ opts << " features"
+ opts
+ end
+
+ def spec_formatter_opts
+ " --require #{File.dirname(__FILE__)}/../rspec_growler.rb --format RSpecGrowler:STDOUT"
+ end
+
+ def cucumber_formatter_opts
+ " --require #{File.dirname(__FILE__)}/../cucumber_growler.rb"
+ end
+
+ def spec_runner
+ if File.exist?("script/spec")
+ "script/spec"
+ else
+ "spec"
+ end
+ end
+
+ def cucumber_runner
+ if File.exist?("script/cucumber")
+ "script/cucumber"
+ else
+ "cucumber"
+ end
+ end
+
+ def ruby_opts
+ other = ENV['RUBYOPT'] ? " #{ENV['RUBYOPT']}" : ''
+ other << ' -rcoral' if options[:coral]
+ %(RUBYOPT='-Ilib:spec#{other}')
+ end
+
+ def git_head_changed?
+ old_git_head = @git_head
+ read_git_head
+ @git_head and old_git_head and @git_head != old_git_head
+ end
+
+ def read_git_head
+ git_head_file = File.join(dir, '.git', 'HEAD')
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
+ end
+ end
+end
+
+# backward compatibility
+Runner = RSpactor::Runner
View
25 lib/rspactor/spork.rb
@@ -0,0 +1,25 @@
+require 'rspactor'
+
+module RSpactor
+ class Spork
+
+ def self.start
+ kill_and_launch
+ Interactor.ticker_msg "** Launching Spork for rspec & cucumber"
+ end
+
+ def self.reload
+ kill_and_launch
+ Interactor.ticker_msg "** Reloading Spork for rspec & cucumber"
+ end
+
+ private
+
+ def self.kill_and_launch
+ system("kill $(ps aux | awk '/spork/&&!/awk/{print $2;}') >/dev/null 2>&1")
+ system("spork >/dev/null 2>&1 < /dev/null &")
+ system("spork cu >/dev/null 2>&1 < /dev/null &")
+ end
+
+ end
+end
View
24 lib/rspec_growler.rb
@@ -0,0 +1,24 @@
+require 'spec/runner/formatter/base_formatter'
+require File.dirname(__FILE__) + '/rspactor/growl'
+
+class RSpecGrowler < Spec::Runner::Formatter::BaseFormatter
+ include RSpactor::Growl
+
+ def dump_summary(duration, total, failures, pending)
+ icon = if failures > 0
+ 'failed'
+ elsif pending > 0
+ 'pending'
+ else
+ 'success'
+ end
+
+ # image_path = File.dirname(__FILE__) + "/../images/#{icon}.png"
+ message = "#{total} examples, #{failures} failures"
+ if pending > 0
+ message << " (#{pending} pending)"
+ end
+
+ notify "Spec Results", message, icon
+ end
+end
View
29 rspactor.gemspec
@@ -0,0 +1,29 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{rspactor}
+ s.version = "0.5.3"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Mislav Marohni\304\207", "Andreas Wolff", "Pelle Braendgaard", "Thibaud Guillaume-Gentil"]
+ s.date = %q{2009-09-08}
+ s.default_executable = %q{rspactor}
+ s.description = %q{read summary!}
+ s.email = %q{guillaumegentil@gmail.com}
+ s.executables = ["rspactor"]
+ s.files = ["Rakefile", "bin/rspactor", "lib/cucumber_growler.rb", "lib/rspactor", "lib/rspactor/celerity.rb", "lib/rspactor/growl.rb", "lib/rspactor/inspector.rb", "lib/rspactor/interactor.rb", "lib/rspactor/listener.rb", "lib/rspactor/runner.rb", "lib/rspactor/spork.rb", "lib/rspactor.rb", "lib/rspec_growler.rb", "images/failed.png", "images/pending.png", "images/success.png", "spec/inspector_spec.rb", "spec/listener_spec.rb", "spec/runner_spec.rb", "LICENSE"]
+ s.homepage = %q{http://github.com/guillaumegentil/rspactor}
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.5}
+ s.summary = %q{RSpactor is a command line tool to automatically run your changed specs & cucumber features (much like autotest).}
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ else
+ end
+ else
+ end
+end
View
131 spec/inspector_spec.rb
@@ -0,0 +1,131 @@
+require 'rspactor/inspector'
+
+describe RSpactor::Inspector do
+ before(:all) do
+ options = { :view => true }
+ @inspector = described_class.new(mock('Runner', :dir => '/project', :options => options))
+ end
+
+ def translate(file)
+ @inspector.translate(file)
+ end
+
+ describe "#translate" do
+ it "should consider all controllers when application_controller changes" do
+ translate('/project/app/controllers/application_controller.rb').should == ['/project/spec/controllers']
+ translate('/project/app/controllers/application.rb').should == ['/project/spec/controllers']
+ end
+
+ it "should translate files under 'app/' directory" do
+ translate('/project/app/controllers/foo_controller.rb').should ==
+ ['/project/spec/controllers/foo_controller_spec.rb']
+ end
+
+ it "should translate templates" do
+ translate('/project/app/views/foo/bar.erb').should == ['/project/spec/views/foo/bar.erb_spec.rb']
+ translate('/project/app/views/foo/bar.html.haml').should ==
+ ['/project/spec/views/foo/bar.html.haml_spec.rb', '/project/spec/views/foo/bar.html_spec.rb']
+ end
+
+ it "should consider all views when application_helper changes" do
+ translate('/project/app/helpers/application_helper.rb').should == ['/project/spec/helpers', '/project/spec/views']
+ end
+
+ it "should consider related templates when a helper changes" do
+ translate('/project/app/helpers/foo_helper.rb').should ==
+ ['/project/spec/helpers/foo_helper_spec.rb', '/project/spec/views/foo']
+ end
+
+ it "should translate files under deep 'lib/' directory" do
+ translate('/project/lib/awesum/rox.rb').should ==
+ ['/project/spec/lib/awesum/rox_spec.rb', '/project/spec/awesum/rox_spec.rb', '/project/spec/rox_spec.rb']
+ end
+
+ it "should translate files under shallow 'lib/' directory" do
+ translate('lib/runner.rb').should == ['/project/spec/lib/runner_spec.rb', '/project/spec/runner_spec.rb']
+ end
+
+ it "should handle relative paths" do
+ translate('foo.rb').should == ['/project/spec/foo_spec.rb']
+ end
+
+ it "should handle files without extension" do
+ translate('foo').should == ['/project/spec/foo_spec.rb']
+ end
+
+ it "should consider all controllers, helpers and views when routes.rb changes" do
+ translate('config/routes.rb').should == ['/project/spec/controllers', '/project/spec/helpers', '/project/spec/views', '/project/spec/routing']
+ end
+
+ it "should consider all models when config/database.yml changes" do
+ translate('config/database.yml').should == ['/project/spec/models']
+ end
+
+ it "should consider all models when db/schema.rb changes" do
+ translate('db/schema.rb').should == ['/project/spec/models']
+ end
+
+ it "should consider all models when spec/factories.rb changes" do
+ translate('spec/factories.rb').should == ['/project/spec/models']
+ end
+
+ it "should consider related model when its observer changes" do
+ translate('app/models/user_observer.rb').should == ['/project/spec/models/user_observer_spec.rb', '/project/spec/models/user_spec.rb']
+ end
+
+ it "should consider all specs when spec_helper changes" do
+ translate('spec/spec_helper.rb').should == ['/project/spec']
+ end
+
+ it "should consider all specs when code under spec/shared/ changes" do
+ translate('spec/shared/foo.rb').should == ['/project/spec']
+ end
+
+ it "should consider all specs when app configuration changes" do
+ translate('config/environment.rb').should == ['/project/spec']
+ translate('config/environments/test.rb').should == ['/project/spec']
+ translate('config/boot.rb').should == ['/project/spec']
+ end
+
+ it "should consider cucumber when a features file change" do
+ translate('features/login.feature').should == ['cucumber']
+ translate('features/steps/webrat_steps.rb').should == ['cucumber']
+ translate('features/support/env.rb').should == ['cucumber']
+ end
+
+ end
+
+ describe "#determine_files" do
+ def determine(file)
+ @inspector.determine_files(file)
+ end
+
+ it "should filter out files that don't exist on the filesystem" do
+ @inspector.should_receive(:translate).with('foo').and_return(%w(valid_spec.rb invalid_spec.rb))
+ File.should_receive(:exists?).with('valid_spec.rb').and_return(true)
+ File.should_receive(:exists?).with('invalid_spec.rb').and_return(false)
+ determine('foo').should == ['valid_spec.rb']
+ end
+
+ it "should filter out files in subdirectories that are already on the list" do
+ @inspector.should_receive(:translate).with('foo').and_return(%w(
+ spec/foo_spec.rb
+ spec/views/moo/bar_spec.rb
+ spec/views/baa/boo_spec.rb
+ spec/models/baz_spec.rb
+ spec/controllers/moo_spec.rb
+ spec/models
+ spec/controllers
+ spec/views/baa
+ ))
+ File.stub!(:exists?).and_return(true)
+ determine('foo').should == %w(
+ spec/foo_spec.rb
+ spec/views/moo/bar_spec.rb
+ spec/models
+ spec/controllers
+ spec/views/baa
+ )
+ end
+ end
+end
View
39 spec/listener_spec.rb
@@ -0,0 +1,39 @@
+require 'rspactor/listener'
+
+describe RSpactor::Listener do
+ before(:all) do
+ @listener = described_class.new(%w(rb erb haml))
+ end
+
+ it "should be timestamped" do
+ @listener.last_check.should be_instance_of(Time)
+ end
+
+ it "should not ignore regular directories" do
+ @listener.ignore_path?('/project/foo/bar').should_not be
+ end
+
+ it "should ignore .git directories" do
+ @listener.ignore_path?('/project/.git/index').should be
+ end
+
+ it "should ignore dotfiles" do
+ @listener.ignore_file?('/project/.foo').should be
+ end
+
+ it "should not ignore files in directories which start with a dot" do
+ @listener.ignore_file?('/project/.foo/bar.rb').should be_false
+ end
+
+ it "should not ignore files without extension" do
+ @listener.ignore_file?('/project/foo.rb').should be_false
+ end
+
+ it "should ignore files without extension" do
+ @listener.ignore_file?('/project/foo').should be
+ end
+
+ it "should ignore files with extensions that don't match those specified" do
+ @listener.ignore_file?('/project/foo.bar').should be
+ end
+end
View
259 spec/runner_spec.rb
@@ -0,0 +1,259 @@
+require 'rspactor/runner'
+
+describe RSpactor::Runner do
+
+ described_class.class_eval do
+ def run_command(cmd)
+ # never shell out in tests
+ cmd
+ end
+ end
+
+ def with_env(name, value)
+ old_value = ENV[name]
+ ENV[name] = value
+ begin
+ yield
+ ensure
+ ENV[name] = old_value
+ end
+ end
+
+ def capture_stderr(io = StringIO.new)
+ @old_stderr, $stderr = $stderr, io
+ begin; yield ensure; restore_stderr; end if block_given?
+ end
+
+ def restore_stderr
+ $stderr = @old_stderr
+ end
+
+ def capture_stdout(io = StringIO.new)
+ @old_stdout, $stdout = $stdout, io
+ begin; yield ensure; restore_stdout; end if block_given?
+ end
+
+ def restore_stdout
+ $stdout = @old_stdout
+ end
+
+ it 'should use the current directory to run in' do
+ mock_instance = mock('RunnerInstance')
+ mock_instance.stub!(:start)
+ RSpactor::Runner.should_receive(:new).with(Dir.pwd, {}).and_return(mock_instance)
+ RSpactor::Runner.start
+ end
+
+ it 'should take an optional directory to run in' do
+ mock_instance = mock('RunnerInstance')
+ mock_instance.stub!(:start)
+ RSpactor::Runner.should_receive(:new).with('/tmp/mu', {}).and_return(mock_instance)
+ RSpactor::Runner.start(:run_in => '/tmp/mu')
+ end
+
+ describe "start" do
+ before(:each) do
+ @runner = described_class.new('/my/path')
+ capture_stdout
+ end
+
+ after(:each) do
+ restore_stdout
+ end
+
+ def setup
+ @runner.start
+ end
+
+ context "Interactor" do
+ before(:each) do
+ @runner.stub!(:load_dotfile)
+ @runner.stub!(:start_listener)
+ @interactor = mock('Interactor')
+ @interactor.should_receive(:start_termination_handler)
+ RSpactor::Interactor.should_receive(:new).and_return(@interactor)
+ end
+
+ it "should start Interactor" do
+ @interactor.should_receive(:wait_for_enter_key).with(instance_of(String), 2, false)
+ setup
+ end
+
+ it "should run all specs if Interactor isn't interrupted" do
+ @interactor.should_receive(:wait_for_enter_key).and_return(nil)
+ @runner.should_receive(:run_spec_command).with('/my/path/spec')
+ setup
+ end
+
+ it "should skip running all specs if Interactor is interrupted" do
+ @interactor.should_receive(:wait_for_enter_key).and_return(true)
+ @runner.should_not_receive(:run_spec_command)
+ setup
+ end
+ end
+
+ it "should initialize Inspector" do
+ @runner.stub!(:load_dotfile)
+ @runner.stub!(:start_interactor)
+ RSpactor::Inspector.should_receive(:new)
+ RSpactor::Listener.stub!(:new).and_return(mock('Listener').as_null_object)
+ setup
+ end
+
+ context "Listener" do
+ before(:each) do
+ @runner.stub!(:load_dotfile)
+ @runner.stub!(:start_interactor)
+ @inspector = mock("Inspector")
+ RSpactor::Inspector.stub!(:new).and_return(@inspector)
+ @listener = mock('Listener')
+ end
+
+ it "should run Listener" do
+ @listener.should_receive(:run).with('/my/path')
+ RSpactor::Listener.should_receive(:new).with(instance_of(Array)).and_return(@listener)
+ setup
+ end
+ end
+
+ it "should output 'watching' message on start" do
+ @runner.stub!(:load_dotfile)
+ @runner.stub!(:start_interactor)
+ @runner.stub!(:start_listener)
+ setup
+ $stdout.string.chomp.should == "** RSpactor is now watching at '/my/path'"
+ end
+
+ context "dotfile" do
+ before(:each) do
+ @runner.stub!(:start_interactor)
+ @runner.stub!(:start_listener)
+ end
+
+ it "should load dotfile if found" do
+ with_env('HOME', '/home/moo') do
+ File.should_receive(:exists?).with('/home/moo/.rspactor').and_return(true)
+ Kernel.should_receive(:load).with('/home/moo/.rspactor')
+ setup
+ end
+ end
+
+ it "should continue even if the dotfile raised errors" do
+ with_env('HOME', '/home/moo') do
+ File.should_receive(:exists?).and_return(true)
+ Kernel.should_receive(:load).with('/home/moo/.rspactor').and_raise(ArgumentError)
+ capture_stderr do
+ lambda { setup }.should_not raise_error
+ $stderr.string.split("\n").should include('Error while loading /home/moo/.rspactor: ArgumentError')
+ end
+ end
+ end
+ end
+ end
+
+ describe "#run_spec_command" do
+ before(:each) do
+ @runner = described_class.new('/my/path')
+ end
+
+ def with_rubyopt(string, &block)
+ with_env('RUBYOPT', string, &block)
+ end
+
+ def run(paths)
+ @runner.run_spec_command(paths)
+ end
+
+ it "should exit if the paths argument is empty" do
+ @runner.should_not_receive(:run_command)
+ run([])
+ end
+
+ it "should specify runner spec runner with joined paths" do
+ run(%w(foo bar)).should include(' spec foo bar ')
+ end
+
+ it "should specify default options: --color" do
+ run('foo').should include(' --color')
+ end
+
+ it "should setup RUBYOPT environment variable" do
+ with_rubyopt(nil) do
+ run('foo').should include("RUBYOPT='-Ilib:spec' ")
+ end
+ end
+
+ it "should concat existing RUBYOPTs" do
+ with_rubyopt('-rubygems -w') do
+ run('foo').should include("RUBYOPT='-Ilib:spec -rubygems -w' ")
+ end
+ end
+
+ it "should include growl formatter" do
+ run('foo').should include(' --format RSpecGrowler:STDOUT')
+ end
+
+ it "should include 'progress' formatter" do
+ run('foo').should include(' -f progress')
+ end
+
+ it "should not include 'progress' formatter if there already are 2 or more formatters" do
+ @runner.should_receive(:spec_formatter_opts).and_return('-f foo --format bar')
+ run('foo').should_not include('--format progress')
+ end
+
+ it "should save status of last run" do
+ @runner.should_receive(:run_command).twice.and_return(true, false)
+ run('foo')
+ @runner.last_run_failed?.should be_false
+ run('bar')
+ @runner.last_run_failed?.should be_true
+ run([])
+ @runner.last_run_failed?.should be_false
+ end
+ end
+
+ describe "#changed_files" do
+ before(:each) do
+ @runner = described_class.new('.')
+ @runner.stub!(:inspector).and_return(mock("Inspector"))
+ end
+
+ def set_inspector_expectation(file, ret)
+ @runner.inspector.should_receive(:determine_files).with(file).and_return(ret)
+ end
+
+ it "should find and run spec files" do
+ set_inspector_expectation('moo.rb', ['spec/moo_spec.rb'])
+ set_inspector_expectation('views/baz.haml', [])
+ set_inspector_expectation('config/bar.yml', ['spec/bar_spec.rb', 'spec/bar_stuff_spec.rb'])
+
+ expected = %w(spec/moo_spec.rb spec/bar_spec.rb spec/bar_stuff_spec.rb)
+ @runner.should_receive(:run_spec_command).with(expected)
+
+ capture_stdout do
+ @runner.stub!(:dir)
+ @runner.send(:changed_files, %w(moo.rb views/baz.haml config/bar.yml))
+ $stdout.string.split("\n").should == expected
+ end
+ end
+
+ it "should run the full suite after a run succeded when the previous one failed" do
+ @runner.inspector.stub!(:determine_files).and_return(['spec/foo_spec.rb'], ['spec/bar_spec.rb'])
+ @runner.stub!(:options).and_return({ :retry_failed => true })
+
+ capture_stdout do
+ @runner.stub!(:run_spec_command)
+ @runner.should_receive(:last_run_failed?).and_return(true, false)
+ @runner.should_receive(:run_all_specs)
+ @runner.send(:changed_files, %w(moo.rb))
+ end
+ end
+ end
+
+ it "should have Runner in global namespace for backwards compatibility" do
+ defined?(::Runner).should be_true
+ ::Runner.should == described_class
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.