guillaumegentil-rspactor 0.2.8 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Mislav Marohnić
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -1,16 +1,57 @@
1
- require 'rubygems'
2
- require 'rake'
3
- require 'echoe'
1
+ task :default => :spec
4
2
 
5
- Echoe.new('rspactor', '0.2.8') do |p|
6
- p.description = "RSpactor is a little command line tool to automatically run your changed specs (much like autotest)."
7
- p.url = "http://github.com/guillaumegentil/rspactor/"
8
- p.author = "Andreas Wolff"
9
- p.email = "treas@dynamicdudes.com"
10
- p.ignore_pattern = ["tmp/*", "script/*"]
11
- p.executable_pattern = "bin/rspactor"
12
- p.runtime_dependencies = ["optiflag"]
13
- p.development_dependencies = []
3
+ desc "starts RSpactor"
4
+ task :spec do
5
+ system "ruby -Ilib bin/rspactor"
14
6
  end
15
7
 
16
- Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each
8
+ desc "generates .gemspec file"
9
+ task :gemspec => "version:read" do
10
+ spec = Gem::Specification.new do |gem|
11
+ gem.name = "rspactor"
12
+ gem.summary = "RSpactor is a command line tool to automatically run your changed specs (much like autotest)."
13
+ gem.email = "mislav.marohnic@gmail.com"
14
+ gem.homepage = "http://github.com/mislav/rspactor"
15
+ gem.authors = ["Mislav Marohnić", "Andreas Wolff", "Pelle Braendgaard", "Thibaud Guillaume-Gentil"]
16
+ gem.has_rdoc = false
17
+
18
+ gem.version = GEM_VERSION
19
+ gem.files = FileList['Rakefile', '{bin,lib,images,spec}/**/*', 'README*', 'LICENSE*']
20
+ gem.executables = Dir['bin/*'].map { |f| File.basename(f) }
21
+ end
22
+
23
+ spec_string = spec.to_ruby
24
+
25
+ begin
26
+ Thread.new { eval("$SAFE = 3\n#{spec_string}", binding) }.join
27
+ rescue
28
+ abort "unsafe gemspec: #{$!}"
29
+ else
30
+ File.open("#{spec.name}.gemspec", 'w') { |file| file.write spec_string }
31
+ end
32
+ end
33
+
34
+ task :bump => ["version:bump", :gemspec]
35
+
36
+ namespace :version do
37
+ task :read do
38
+ unless defined? GEM_VERSION
39
+ GEM_VERSION = File.read("VERSION")
40
+ end
41
+ end
42
+
43
+ task :bump => :read do
44
+ if ENV['VERSION']
45
+ GEM_VERSION.replace ENV['VERSION']
46
+ else
47
+ GEM_VERSION.sub!(/\d+$/) { |num| num.to_i + 1 }
48
+ end
49
+
50
+ File.open("VERSION", 'w') { |v| v.write GEM_VERSION }
51
+ end
52
+ end
53
+
54
+ task :release => :bump do
55
+ system %(git commit VERSION *.gemspec -m "release v#{GEM_VERSION}")
56
+ system %(git tag -am "release v#{GEM_VERSION}" v#{GEM_VERSION})
57
+ end
data/bin/rspactor CHANGED
@@ -1,21 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ require 'rspactor/runner'
2
3
 
3
- require 'optiflag'
4
- require File.join(File.dirname(__FILE__), '..', 'lib', 'interactor')
5
- require File.join(File.dirname(__FILE__), '..', 'lib', 'listener')
6
- require File.join(File.dirname(__FILE__), '..', 'lib', 'inspector')
7
- require File.join(File.dirname(__FILE__), '..', 'lib', 'runner')
8
-
9
- module RSpactor extend OptiFlagSet
10
- optional_switch_flag "drb" do
11
- description "use spec_server"
12
- end
13
- optional_switch_flag "clear" do
14
- description "clear shell before each spec execution"
15
- end
16
-
17
- and_process!
18
- end
19
-
20
-
21
- Runner.load(:drb => ARGV.flags.drb?, :clear => ARGV.flags.clear?)
4
+ RSpactor::Runner.start({
5
+ :coral => ARGV.delete('--coral'),
6
+ :run_in => ARGV.last
7
+ })
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rspactor'
3
+ Growl = RSpactor::Growl
4
+
5
+ root = ENV['HOME']
6
+ $mappings = []
7
+ $libs = []
8
+
9
+ def map(regex, &block)
10
+ $mappings << [regex, block]
11
+ end
12
+
13
+ def libs
14
+ $libs
15
+ end
16
+
17
+ def title
18
+ $title
19
+ end
20
+
21
+ listener = RSpactor::Listener.new do |changed_files|
22
+ changed_files.reject! do |file|
23
+ file.index(root + "/Library/") == 0
24
+ end
25
+
26
+ if changed_files.size == 1
27
+ changed_file = changed_files.first
28
+ dir = changed_file
29
+ hook = nil
30
+
31
+ until hook or (dir = File.dirname(dir)) == root
32
+ candidate = dir + "/.rspactor"
33
+ hook = candidate if File.exists?(candidate)
34
+ end
35
+
36
+ if hook
37
+ targets = []
38
+ $title = "Test results"
39
+ $mappings.clear
40
+ $libs.replace ['lib']
41
+ load hook
42
+
43
+ unless $mappings.empty?
44
+ relative_path = changed_file.sub(dir + '/', '')
45
+
46
+ for regex, block in $mappings
47
+ if match = relative_path.match(regex)
48
+ targets.concat Array(block.call(relative_path, match))
49
+ break
50
+ end
51
+ end
52
+
53
+ existing_targets = targets.select { |file| File.exist?(File.join(dir, file)) }
54
+ else
55
+ inspector = RSpactor::Inspector.new(dir)
56
+ existing_targets = inspector.determine_spec_files(changed_file).map do |file|
57
+ file.sub(dir + '/', '')
58
+ end
59
+ end
60
+
61
+ if not existing_targets.empty?
62
+ case existing_targets.first
63
+ when %r{^test/}
64
+ $libs << 'test'
65
+ when %r{^spec/}
66
+ $libs << 'spec'
67
+ end
68
+
69
+ Dir.chdir(dir) do
70
+ unless 'spec' == $libs.last
71
+ command = "ruby -I#{$libs.join(':')} -e 'ARGV.each{|f| load f}' "
72
+ else
73
+ command = "RUBYOPT='-I#{$libs.join(':')}' spec --color "
74
+ end
75
+ command << existing_targets.join(' ')
76
+ # puts command
77
+ puts changed_file
78
+ system command
79
+ end
80
+
81
+ if $?.success?
82
+ Growl::notify $title, "You rock!", Growl::image_path('success')
83
+ else
84
+ Growl::notify $title, "YOU LOSE", Growl::image_path('failed')
85
+ end
86
+ elsif $mappings.empty?
87
+ $stderr.puts "-- don't know how to run #{changed_file}"
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ listener.run(root)
File without changes
Binary file
File without changes
@@ -0,0 +1,14 @@
1
+ module RSpactor
2
+ module Growl
3
+ extend self
4
+
5
+ def notify(title, msg, img, pri = 0)
6
+ system("growlnotify -w -n rspactor --image #{img} -p #{pri} -m #{msg.inspect} #{title} &")
7
+ end
8
+
9
+ # failed | pending | success
10
+ def image_path(icon)
11
+ File.expand_path File.dirname(__FILE__) + "/../../images/#{icon}.png"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,89 @@
1
+ module RSpactor
2
+ # Maps the changed filenames to list of specs to run in the next go.
3
+ # Assumes Rails-like directory structure
4
+ class Inspector
5
+ EXTENSIONS = %w(rb erb builder haml rhtml rxml yml conf opts)
6
+
7
+ def initialize(dir)
8
+ @root = dir
9
+ end
10
+
11
+ def determine_spec_files(file)
12
+ candidates = translate(file)
13
+ candidates.reject { |candidate| candidate.index('.') }.each do |dir|
14
+ candidates.reject! { |candidate| candidate.index("#{dir}/") == 0 }
15
+ end
16
+ spec_files = candidates.select { |candidate| File.exists? candidate }
17
+
18
+ if spec_files.empty?
19
+ $stderr.puts "doesn't exist: #{candidates.inspect}"
20
+ end
21
+ spec_files
22
+ end
23
+
24
+ # mappings for Rails are inspired by autotest mappings in rspec-rails
25
+ def translate(file)
26
+ file = file.sub(%r:^#{Regexp.escape(@root)}/:, '')
27
+ candidates = []
28
+
29
+ if spec_file?(file)
30
+ candidates << file
31
+ else
32
+ spec_file = append_spec_file_extension(file)
33
+
34
+ case file
35
+ when %r:^app/:
36
+ if file =~ %r:^app/controllers/application(_controller)?.rb$:
37
+ candidates << 'controllers'
38
+ elsif file == 'app/helpers/application_helper.rb'
39
+ candidates << 'helpers' << 'views'
40
+ else
41
+ candidates << spec_file.sub('app/', '')
42
+
43
+ if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
44
+ candidates << append_spec_file_extension($1)
45
+ elsif file =~ %r:app/helpers/(\w+)_helper.rb:
46
+ candidates << "views/#{$1}"
47
+ elsif file =~ /_observer.rb$/
48
+ candidates << candidates.last.sub('_observer', '')
49
+ end
50
+ end
51
+ when %r:^lib/:
52
+ candidates << spec_file
53
+ # lib/foo/bar_spec.rb -> lib/bar_spec.rb
54
+ candidates << candidates.last.sub($&, '')
55
+ # lib/bar_spec.rb -> bar_spec.rb
56
+ candidates << candidates.last.sub(%r:\w+/:, '') if candidates.last.index('/')
57
+ when 'config/routes.rb'
58
+ candidates << 'controllers' << 'helpers' << 'views'
59
+ when 'config/database.yml', 'db/schema.rb'
60
+ candidates << 'models'
61
+ when %r:^(spec/(spec_helper|shared/.*)|config/(boot|environment(s/test)?))\.rb$:, 'spec/spec.opts'
62
+ candidates << 'spec'
63
+ else
64
+ candidates << spec_file
65
+ end
66
+ end
67
+
68
+ candidates.map do |candidate|
69
+ if candidate.index('spec') == 0
70
+ File.join(@root, candidate)
71
+ else
72
+ File.join(@root, 'spec', candidate)
73
+ end
74
+ end
75
+ end
76
+
77
+ def append_spec_file_extension(file)
78
+ if File.extname(file) == ".rb"
79
+ file.sub(/.rb$/, "_spec.rb")
80
+ else
81
+ file + "_spec.rb"
82
+ end
83
+ end
84
+
85
+ def spec_file?(file)
86
+ file =~ /^spec\/.+_spec.rb$/
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,64 @@
1
+ require 'timeout'
2
+
3
+ module RSpactor
4
+ class Interactor
5
+ def initialize(dir)
6
+ @root = dir
7
+ ticker
8
+ end
9
+
10
+ def wait_for_enter_key(msg, seconds_to_wait)
11
+ begin
12
+ Timeout::timeout(seconds_to_wait) do
13
+ ticker(:start => true, :msg => msg)
14
+ $stdin.gets
15
+ return true
16
+ end
17
+ rescue Timeout::Error
18
+ false
19
+ ensure
20
+ ticker(:stop => true)
21
+ end
22
+ end
23
+
24
+ def start_termination_handler
25
+ @main_thread = Thread.current
26
+ Thread.new do
27
+ loop do
28
+ sleep 0.5
29
+ if $stdin.gets
30
+ if wait_for_enter_key("** Running all specs.. Hit <enter> again to exit RSpactor", 3)
31
+ @main_thread.exit
32
+ exit
33
+ end
34
+ Runner.new(@root).run_all_specs
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def ticker(opts = {})
43
+ if opts[:stop]
44
+ $stdout.puts "\n"
45
+ @pointer_running = false
46
+ elsif opts[:start]
47
+ @pointer_running = true
48
+ write(opts[:msg]) if opts[:msg]
49
+ else
50
+ Thread.new do
51
+ loop do
52
+ write('.') if @pointer_running == true
53
+ sleep 1.0
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def write(msg)
60
+ $stdout.print(msg)
61
+ $stdout.flush
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,88 @@
1
+ require 'osx/foundation'
2
+ OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
3
+
4
+ module RSpactor
5
+ # based on http://rails.aizatto.com/2007/11/28/taming-the-autotest-beast-with-fsevents/
6
+ class Listener
7
+ attr_reader :last_check, :callback, :valid_extensions
8
+
9
+ def initialize(valid_extensions = nil)
10
+ @valid_extensions = valid_extensions
11
+ timestamp_checked
12
+
13
+ @callback = lambda do |stream, ctx, num_events, paths, marks, event_ids|
14
+ changed_files = extract_changed_files_from_paths(split_paths(paths, num_events))
15
+ timestamp_checked
16
+ yield changed_files unless changed_files.empty?
17
+ end
18
+ end
19
+
20
+ def run(directories)
21
+ dirs = Array(directories)
22
+ stream = OSX::FSEventStreamCreate(OSX::KCFAllocatorDefault, callback, nil, dirs, OSX::KFSEventStreamEventIdSinceNow, 0.5, 0)
23
+ unless stream
24
+ $stderr.puts "Failed to create stream"
25
+ exit(1)
26
+ end
27
+
28
+ OSX::FSEventStreamScheduleWithRunLoop(stream, OSX::CFRunLoopGetCurrent(), OSX::KCFRunLoopDefaultMode)
29
+ unless OSX::FSEventStreamStart(stream)
30
+ $stderr.puts "Failed to start stream"
31
+ exit(1)
32
+ end
33
+
34
+ begin
35
+ OSX::CFRunLoopRun()
36
+ rescue Interrupt
37
+ OSX::FSEventStreamStop(stream)
38
+ OSX::FSEventStreamInvalidate(stream)
39
+ OSX::FSEventStreamRelease(stream)
40
+ end
41
+ end
42
+
43
+ def timestamp_checked
44
+ @last_check = Time.now
45
+ end
46
+
47
+ def split_paths(paths, num_events)
48
+ paths.regard_as('*')
49
+ rpaths = []
50
+ num_events.times { |i| rpaths << paths[i] }
51
+ rpaths
52
+ end
53
+
54
+ def extract_changed_files_from_paths(paths)
55
+ changed_files = []
56
+ paths.each do |path|
57
+ next if ignore_path?(path)
58
+ Dir.glob(path + "*").each do |file|
59
+ next if ignore_file?(file)
60
+ changed_files << file if file_changed?(file)
61
+ end
62
+ end
63
+ changed_files
64
+ end
65
+
66
+ def file_changed?(file)
67
+ File.stat(file).mtime > last_check
68
+ rescue Errno::ENOENT
69
+ false
70
+ end
71
+
72
+ def ignore_path?(path)
73
+ path =~ /(?:^|\/)\.(git|svn)/
74
+ end
75
+
76
+ def ignore_file?(file)
77
+ File.basename(file).index('.') == 0 or not valid_extension?(file)
78
+ end
79
+
80
+ def file_extension(file)
81
+ file =~ /\.(\w+)$/ and $1
82
+ end
83
+
84
+ def valid_extension?(file)
85
+ valid_extensions.nil? or valid_extensions.include?(file_extension(file))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,141 @@
1
+ require 'rspactor'
2
+
3
+ module RSpactor
4
+ class Runner
5
+ def self.start(options = {})
6
+ run_in = options.delete(:run_in) || Dir.pwd
7
+ new(run_in, options).start
8
+ end
9
+
10
+ attr_reader :dir, :options, :inspector, :interactor
11
+
12
+ def initialize(dir, options = {})
13
+ @dir = dir
14
+ @options = options
15
+ read_git_head
16
+ end
17
+
18
+ def start
19
+ load_dotfile
20
+ puts "** RSpactor is now watching at '#{dir}'"
21
+ start_interactor
22
+ start_listener
23
+ end
24
+
25
+ def start_interactor
26
+ @interactor = Interactor.new(dir)
27
+ aborted = @interactor.wait_for_enter_key("** Hit <enter> to skip initial spec run", 3)
28
+ @interactor.start_termination_handler
29
+ run_all_specs unless aborted
30
+ end
31
+
32
+ def start_listener
33
+ @inspector = Inspector.new(dir)
34
+
35
+ Listener.new(Inspector::EXTENSIONS) do |files|
36
+ spec_changed_files(files) unless git_head_changed?
37
+ end.run(dir)
38
+ end
39
+
40
+ def load_dotfile
41
+ dotfile = File.join(ENV['HOME'], '.rspactor')
42
+ if File.exists?(dotfile)
43
+ begin
44
+ Kernel.load dotfile
45
+ rescue => e
46
+ $stderr.puts "Error while loading #{dotfile}: #{e}"
47
+ end
48
+ end
49
+ end
50
+
51
+ def run_all_specs
52
+ run_spec_command(File.join(dir, 'spec'))
53
+ end
54
+
55
+ def run_spec_command(paths)
56
+ paths = Array(paths)
57
+ if paths.empty?
58
+ @last_run_failed = nil
59
+ else
60
+ cmd = [ruby_opts, spec_runner, paths, spec_opts].flatten.join(' ')
61
+ @last_run_failed = run_command(cmd)
62
+ end
63
+ end
64
+
65
+ def last_run_failed?
66
+ @last_run_failed == false
67
+ end
68
+
69
+ protected
70
+
71
+ def run_command(cmd)
72
+ $stderr.puts "#{cmd}"
73
+ system(cmd)
74
+ $?.success?
75
+ end
76
+
77
+ def spec_changed_files(files)
78
+ files_to_spec = files.inject([]) do |all, file|
79
+ all.concat inspector.determine_spec_files(file)
80
+ end
81
+ unless files_to_spec.empty?
82
+ puts files_to_spec.join("\n")
83
+
84
+ previous_run_failed = last_run_failed?
85
+ run_spec_command(files_to_spec)
86
+
87
+ if options[:retry_failed] and previous_run_failed and not last_run_failed?
88
+ run_all_specs
89
+ end
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def spec_opts
96
+ if File.exist?('spec/spec.opts')
97
+ opts = File.read('spec/spec.opts').gsub("\n", ' ')
98
+ else
99
+ opts = "--color"
100
+ end
101
+
102
+ opts << ' ' << formatter_opts
103
+ # only add the "progress" formatter unless no other (besides growl) is specified
104
+ opts << ' -f progress' unless opts.scan(/\s(?:-f|--format)\b/).length > 1
105
+
106
+ opts
107
+ end
108
+
109
+ def formatter_opts
110
+ "-r #{File.dirname(__FILE__)}/../rspec_growler.rb -f RSpecGrowler:STDOUT"
111
+ end
112
+
113
+ def spec_runner
114
+ if File.exist?("script/spec")
115
+ "script/spec"
116
+ else
117
+ "spec"
118
+ end
119
+ end
120
+
121
+ def ruby_opts
122
+ other = ENV['RUBYOPT'] ? " #{ENV['RUBYOPT']}" : ''
123
+ other << ' -rcoral' if options[:coral]
124
+ %(RUBYOPT='-Ilib:spec#{other}')
125
+ end
126
+
127
+ def git_head_changed?
128
+ old_git_head = @git_head
129
+ read_git_head
130
+ @git_head and old_git_head and @git_head != old_git_head
131
+ end
132
+
133
+ def read_git_head
134
+ git_head_file = File.join(dir, '.git', 'HEAD')
135
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
136
+ end
137
+ end
138
+ end
139
+
140
+ # backward compatibility
141
+ Runner = RSpactor::Runner
data/lib/rspactor.rb ADDED
@@ -0,0 +1,7 @@
1
+ module RSpactor
2
+ autoload :Interactor, 'rspactor/interactor'
3
+ autoload :Listener, 'rspactor/listener'
4
+ autoload :Inspector, 'rspactor/inspector'
5
+ autoload :Runner, 'rspactor/runner'
6
+ autoload :Growl, 'rspactor/growl'
7
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec/runner/formatter/base_formatter'
2
+ require File.dirname(__FILE__) + '/rspactor/growl'
3
+
4
+ class RSpecGrowler < Spec::Runner::Formatter::BaseFormatter
5
+ include RSpactor::Growl
6
+
7
+ def dump_summary(duration, total, failures, pending)
8
+ icon = if failures > 0
9
+ 'failed'
10
+ elsif pending > 0
11
+ 'pending'
12
+ else
13
+ 'success'
14
+ end
15
+
16
+ image_path = File.dirname(__FILE__) + "/../images/#{icon}.png"
17
+ message = "#{total} examples, #{failures} failures"
18
+
19
+ if pending > 0
20
+ message << " (#{pending} pending)"
21
+ end
22
+
23
+ notify "Test Results", message, image_path(icon)
24
+ end
25
+ end
26
+