lacomartincik-rspactor 0.3.3.1

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 ADDED
@@ -0,0 +1,57 @@
1
+ task :default => :spec
2
+
3
+ desc "starts RSpactor"
4
+ task :spec do
5
+ system "ruby -Ilib bin/rspactor"
6
+ end
7
+
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"]
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 ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rspactor/runner'
3
+
4
+ RSpactor::Runner.start({
5
+ :coral => ARGV.delete('--coral'),
6
+ :run_in => ARGV.last
7
+ })
@@ -0,0 +1,90 @@
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)
57
+ end
58
+
59
+ unless existing_targets.empty?
60
+ case existing_targets.first
61
+ when %r{^test/}
62
+ $libs << 'test'
63
+ when %r{^spec/}
64
+ $libs << 'spec'
65
+ end
66
+
67
+ Dir.chdir(dir) do
68
+ unless 'spec' == $libs.last
69
+ command = "ruby -I#{$libs.join(':')} -e 'ARGV.each{|f| load f}' "
70
+ else
71
+ command = "RUBYOPT='-I#{$libs.join(':')}' spec --color "
72
+ end
73
+ command << existing_targets.join(' ')
74
+ # puts command
75
+ system command
76
+ end
77
+
78
+ if $?.success?
79
+ Growl::notify $title, "You rock!", Growl::image_path('success')
80
+ else
81
+ Growl::notify $title, "YOU LOSE", Growl::image_path('failed')
82
+ end
83
+ else
84
+ $stderr.puts "-- don't know how to run #{changed_file}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ listener.run(root)
data/images/failed.png ADDED
Binary file
Binary file
Binary file
@@ -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,63 @@
1
+ require 'timeout'
2
+
3
+ module RSpactor
4
+ class Interactor
5
+ def initialize
6
+ ticker
7
+ end
8
+
9
+ def wait_for_enter_key(msg, seconds_to_wait)
10
+ begin
11
+ Timeout::timeout(seconds_to_wait) do
12
+ ticker(:start => true, :msg => msg)
13
+ $stdin.gets
14
+ return true
15
+ end
16
+ rescue Timeout::Error
17
+ false
18
+ ensure
19
+ ticker(:stop => true)
20
+ end
21
+ end
22
+
23
+ def start_termination_handler
24
+ @main_thread = Thread.current
25
+ Thread.new do
26
+ loop do
27
+ sleep 0.5
28
+ if $stdin.gets
29
+ if wait_for_enter_key("** Running all specs.. Hit <enter> again to exit RSpactor", 3)
30
+ @main_thread.exit
31
+ exit
32
+ end
33
+ Runner.run_all_specs
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def ticker(opts = {})
42
+ if opts[:stop]
43
+ $stdout.puts "\n"
44
+ @pointer_running = false
45
+ elsif opts[:start]
46
+ @pointer_running = true
47
+ write(opts[:msg]) if opts[:msg]
48
+ else
49
+ Thread.new do
50
+ loop do
51
+ write('.') if @pointer_running == true
52
+ sleep 1.0
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def write(msg)
59
+ $stdout.print(msg)
60
+ $stdout.flush
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,86 @@
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
+ end
69
+
70
+ def ignore_path?(path)
71
+ path =~ /(?:^|\/)\.(git|svn)/
72
+ end
73
+
74
+ def ignore_file?(file)
75
+ File.basename(file).index('.') == 0 or not valid_extension?(file)
76
+ end
77
+
78
+ def file_extension(file)
79
+ file =~ /\.(\w+)$/ and $1
80
+ end
81
+
82
+ def valid_extension?(file)
83
+ valid_extensions.nil? or valid_extensions.include?(file_extension(file))
84
+ end
85
+ end
86
+ 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 RSpecInspector
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,176 @@
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, :rspec
11
+
12
+ def initialize(dir, options = {})
13
+ @dir = dir
14
+ @options = options
15
+ @rspec = File.exists?(File.join(dir, 'spec'))
16
+ read_git_head
17
+ end
18
+
19
+ def start
20
+ load_dotfile
21
+ puts "** RSpactor is now watching at '#{dir}'"
22
+ start_interactor
23
+ start_listener
24
+ end
25
+
26
+ def start_interactor
27
+ @interactor = Interactor.new
28
+ aborted = @interactor.wait_for_enter_key("** Hit <enter> to skip initial spec run", 3)
29
+ @interactor.start_termination_handler
30
+ run_all_tests unless aborted
31
+ end
32
+
33
+ def start_listener
34
+ @inspector_class = rspec? ? RSpecInspector : TestInspector
35
+ @inspector = @inspector_class.new(dir)
36
+
37
+ Listener.new(@inspector_class::EXTENSIONS) do |files|
38
+ test_changed_files(files) unless git_head_changed?
39
+ end.run(dir)
40
+ end
41
+
42
+ def load_dotfile
43
+ dotfile = File.join(ENV['HOME'], '.rspactor')
44
+ if File.exists?(dotfile)
45
+ begin
46
+ Kernel.load dotfile
47
+ rescue => e
48
+ $stderr.puts "Error while loading #{dotfile}: #{e}"
49
+ end
50
+ end
51
+ end
52
+
53
+ def run_all_tests
54
+ if rspec?
55
+ run_spec_command(File.join(dir, 'spec'))
56
+ else
57
+ patterns = ["test/unit/**/*_test.rb", "test/lib/**/*_test.rb",
58
+ "test/functional/**/*_test.rb", "test/integration/**/*_test.rb"]
59
+
60
+ file_list = []
61
+ patterns.each do |pattern|
62
+ file_list = Dir[pattern]
63
+ end
64
+ run_test_command(file_list)
65
+ end
66
+ end
67
+
68
+ def run_spec_command(paths)
69
+ paths = Array(paths)
70
+ if paths.empty?
71
+ @last_run_failed = nil
72
+ else
73
+ cmd = [ruby_opts, spec_runner, paths, spec_opts].flatten.join(' ')
74
+ @last_run_failed = run_command(cmd)
75
+ end
76
+ end
77
+
78
+ def run_test_command(paths)
79
+ paths = Array(paths)
80
+ if paths.empty?
81
+ @last_run_failed = nil
82
+ else
83
+ cmd = [ruby_opts, test_runner, paths].flatten.join(' ')
84
+ @last_run_failed = run_command(cmd)
85
+ end
86
+ end
87
+
88
+ def last_run_failed?
89
+ @last_run_failed == false
90
+ end
91
+
92
+ protected
93
+
94
+ def run_command(cmd)
95
+ system(cmd)
96
+ $?.success?
97
+ end
98
+
99
+ def test_changed_files(files)
100
+ files_to_spec = files.inject([]) do |all, file|
101
+ all.concat inspector.determine_spec_files(file)
102
+ end
103
+ unless files_to_spec.empty?
104
+ puts files_to_spec.join("\n")
105
+
106
+ previous_run_failed = last_run_failed?
107
+ run_spec_command(files_to_spec)
108
+
109
+ if options[:retry_failed] and previous_run_failed and not last_run_failed?
110
+ run_all_tests
111
+ end
112
+ end
113
+ end
114
+
115
+ def rspec?
116
+ rspec ? true : false
117
+ end
118
+
119
+ private
120
+
121
+ def spec_opts
122
+ if File.exist?('spec/spec.opts')
123
+ opts = File.read('spec/spec.opts').gsub("\n", ' ')
124
+ else
125
+ opts = "--color"
126
+ end
127
+
128
+ opts << ' ' << formatter_opts
129
+ # only add the "progress" formatter unless no other (besides growl) is specified
130
+ opts << ' -f progress' unless opts.scan(/\s(?:-f|--format)\b/).length > 1
131
+
132
+ opts
133
+ end
134
+
135
+ def formatter_opts
136
+ "-r #{File.dirname(__FILE__)}/../rspec_growler.rb -f RSpecGrowler:STDOUT"
137
+ end
138
+
139
+ def spec_runner
140
+ if File.exist?("script/spec")
141
+ "script/spec"
142
+ else
143
+ "spec"
144
+ end
145
+ end
146
+
147
+ def test_runner
148
+ 'ruby'
149
+ end
150
+
151
+ def ruby_opts
152
+ other = ENV['RUBYOPT'] ? " #{ENV['RUBYOPT']}" : ''
153
+ other << ' -rcoral' if options[:coral]
154
+ if rspec?
155
+ %(RUBYOPT='-Ilib:spec#{other}')
156
+ else
157
+ %(RUBYOPT='-Ilib:test#{other}')
158
+ end
159
+ end
160
+
161
+ def git_head_changed?
162
+ old_git_head = @git_head
163
+ read_git_head
164
+ @git_head and old_git_head and @git_head != old_git_head
165
+ end
166
+
167
+ def read_git_head
168
+ git_head_file = File.join(dir, '.git', 'HEAD')
169
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
170
+ end
171
+
172
+ end
173
+ end
174
+
175
+ # backward compatibility
176
+ Runner = RSpactor::Runner
@@ -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 TestInspector
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_test_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
+ test_files = candidates.select { |candidate| File.exists? candidate }
17
+
18
+ if test_files.empty?
19
+ $stderr.puts "doesn't exist: #{candidates.inspect}"
20
+ end
21
+ test_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 test_file?(file)
30
+ candidates << file
31
+ else
32
+ test_file = append_test_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 << test_file.sub('app/', '')
42
+
43
+ if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
44
+ candidates << append_test_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 << test_file
53
+ # lib/foo/bar_test.rb -> lib/bar_test.rb
54
+ candidates << candidates.last.sub($&, '')
55
+ # lib/bar_test.rb -> bar_test.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$:
62
+ candidates << 'test'
63
+ else
64
+ candidates << test_file
65
+ end
66
+ end
67
+
68
+ candidates.map do |candidate|
69
+ if candidate.index('test') == 0
70
+ File.join(@root, candidate)
71
+ else
72
+ File.join(@root, 'test', candidate)
73
+ end
74
+ end
75
+ end
76
+
77
+ def append_test_file_extension(file)
78
+ if File.extname(file) == ".rb"
79
+ file.sub(/.rb$/, "_test.rb")
80
+ else
81
+ file + "_test.rb"
82
+ end
83
+ end
84
+
85
+ def test_file?(file)
86
+ file =~ /^test\/.+_test.rb$/
87
+ end
88
+ end
89
+ end
data/lib/rspactor.rb ADDED
@@ -0,0 +1,8 @@
1
+ module RSpactor
2
+ autoload :Interactor, 'rspactor/interactor'
3
+ autoload :Listener, 'rspactor/listener'
4
+ autoload :RSpecInspector, 'rspactor/rspec_inspector'
5
+ autoload :TestInspector, 'rspactor/test_inspector'
6
+ autoload :Runner, 'rspactor/runner'
7
+ autoload :Growl, 'rspactor/growl'
8
+ 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
+
@@ -0,0 +1,39 @@
1
+ require 'rspactor/listener'
2
+
3
+ describe RSpactor::Listener do
4
+ before(:all) do
5
+ @listener = described_class.new(%w(rb erb haml))
6
+ end
7
+
8
+ it "should be timestamped" do
9
+ @listener.last_check.should be_instance_of(Time)
10
+ end
11
+
12
+ it "should not ignore regular directories" do
13
+ @listener.ignore_path?('/project/foo/bar').should_not be
14
+ end
15
+
16
+ it "should ignore .git directories" do
17
+ @listener.ignore_path?('/project/.git/index').should be
18
+ end
19
+
20
+ it "should ignore dotfiles" do
21
+ @listener.ignore_file?('/project/.foo').should be
22
+ end
23
+
24
+ it "should not ignore files in directories which start with a dot" do
25
+ @listener.ignore_file?('/project/.foo/bar.rb').should be_false
26
+ end
27
+
28
+ it "should not ignore files without extension" do
29
+ @listener.ignore_file?('/project/foo.rb').should be_false
30
+ end
31
+
32
+ it "should ignore files without extension" do
33
+ @listener.ignore_file?('/project/foo').should be
34
+ end
35
+
36
+ it "should ignore files with extensions that don't match those specified" do
37
+ @listener.ignore_file?('/project/foo.bar').should be
38
+ end
39
+ end
@@ -0,0 +1,119 @@
1
+ require 'rspactor/rspec_inspector'
2
+
3
+ describe RSpactor::RSpecInspector do
4
+ before(:all) do
5
+ @inspector = described_class.new('/project')
6
+ end
7
+
8
+ def translate(file)
9
+ @inspector.translate(file)
10
+ end
11
+
12
+ describe "#translate" do
13
+ it "should consider all controllers when application_controller changes" do
14
+ translate('/project/app/controllers/application_controller.rb').should == ['/project/spec/controllers']
15
+ translate('/project/app/controllers/application.rb').should == ['/project/spec/controllers']
16
+ end
17
+
18
+ it "should translate files under 'app/' directory" do
19
+ translate('/project/app/controllers/foo_controller.rb').should ==
20
+ ['/project/spec/controllers/foo_controller_spec.rb']
21
+ end
22
+
23
+ it "should translate templates" do
24
+ translate('/project/app/views/foo/bar.erb').should == ['/project/spec/views/foo/bar.erb_spec.rb']
25
+ translate('/project/app/views/foo/bar.html.haml').should ==
26
+ ['/project/spec/views/foo/bar.html.haml_spec.rb', '/project/spec/views/foo/bar.html_spec.rb']
27
+ end
28
+
29
+ it "should consider all views when application_helper changes" do
30
+ translate('/project/app/helpers/application_helper.rb').should == ['/project/spec/helpers', '/project/spec/views']
31
+ end
32
+
33
+ it "should consider related templates when a helper changes" do
34
+ translate('/project/app/helpers/foo_helper.rb').should ==
35
+ ['/project/spec/helpers/foo_helper_spec.rb', '/project/spec/views/foo']
36
+ end
37
+
38
+ it "should translate files under deep 'lib/' directory" do
39
+ translate('/project/lib/awesum/rox.rb').should ==
40
+ ['/project/spec/lib/awesum/rox_spec.rb', '/project/spec/awesum/rox_spec.rb', '/project/spec/rox_spec.rb']
41
+ end
42
+
43
+ it "should translate files under shallow 'lib/' directory" do
44
+ translate('lib/runner.rb').should == ['/project/spec/lib/runner_spec.rb', '/project/spec/runner_spec.rb']
45
+ end
46
+
47
+ it "should handle relative paths" do
48
+ translate('foo.rb').should == ['/project/spec/foo_spec.rb']
49
+ end
50
+
51
+ it "should handle files without extension" do
52
+ translate('foo').should == ['/project/spec/foo_spec.rb']
53
+ end
54
+
55
+ it "should consider all controllers, helpers and views when routes.rb changes" do
56
+ translate('config/routes.rb').should == ['/project/spec/controllers', '/project/spec/helpers', '/project/spec/views']
57
+ end
58
+
59
+ it "should consider all models when config/database.yml changes" do
60
+ translate('config/database.yml').should == ['/project/spec/models']
61
+ end
62
+
63
+ it "should consider all models when db/schema.rb changes" do
64
+ translate('db/schema.rb').should == ['/project/spec/models']
65
+ end
66
+
67
+ it "should consider related model when its observer changes" do
68
+ translate('app/models/user_observer.rb').should == ['/project/spec/models/user_observer_spec.rb', '/project/spec/models/user_spec.rb']
69
+ end
70
+
71
+ it "should consider all specs when spec_helper changes" do
72
+ translate('spec/spec_helper.rb').should == ['/project/spec']
73
+ end
74
+
75
+ it "should consider all specs when code under spec/shared/ changes" do
76
+ translate('spec/shared/foo.rb').should == ['/project/spec']
77
+ end
78
+
79
+ it "should consider all specs when app configuration changes" do
80
+ translate('config/environment.rb').should == ['/project/spec']
81
+ translate('config/environments/test.rb').should == ['/project/spec']
82
+ translate('config/boot.rb').should == ['/project/spec']
83
+ end
84
+ end
85
+
86
+ describe "#determine_spec_files" do
87
+ def determine(file)
88
+ @inspector.determine_spec_files(file)
89
+ end
90
+
91
+ it "should filter out files that don't exist on the filesystem" do
92
+ @inspector.should_receive(:translate).with('foo').and_return(%w(valid_spec.rb invalid_spec.rb))
93
+ File.should_receive(:exists?).with('valid_spec.rb').and_return(true)
94
+ File.should_receive(:exists?).with('invalid_spec.rb').and_return(false)
95
+ determine('foo').should == ['valid_spec.rb']
96
+ end
97
+
98
+ it "should filter out files in subdirectories that are already on the list" do
99
+ @inspector.should_receive(:translate).with('foo').and_return(%w(
100
+ spec/foo_spec.rb
101
+ spec/views/moo/bar_spec.rb
102
+ spec/views/baa/boo_spec.rb
103
+ spec/models/baz_spec.rb
104
+ spec/controllers/moo_spec.rb
105
+ spec/models
106
+ spec/controllers
107
+ spec/views/baa
108
+ ))
109
+ File.stub!(:exists?).and_return(true)
110
+ determine('foo').should == %w(
111
+ spec/foo_spec.rb
112
+ spec/views/moo/bar_spec.rb
113
+ spec/models
114
+ spec/controllers
115
+ spec/views/baa
116
+ )
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,258 @@
1
+ require 'rspactor/runner'
2
+
3
+ describe RSpactor::Runner do
4
+
5
+ described_class.class_eval do
6
+ def run_command(cmd)
7
+ # never shell out in tests
8
+ cmd
9
+ end
10
+ end
11
+
12
+ def with_env(name, value)
13
+ old_value = ENV[name]
14
+ ENV[name] = value
15
+ begin
16
+ yield
17
+ ensure
18
+ ENV[name] = old_value
19
+ end
20
+ end
21
+
22
+ def capture_stderr(io = StringIO.new)
23
+ @old_stderr, $stderr = $stderr, io
24
+ begin; yield ensure; restore_stderr; end if block_given?
25
+ end
26
+
27
+ def restore_stderr
28
+ $stderr = @old_stderr
29
+ end
30
+
31
+ def capture_stdout(io = StringIO.new)
32
+ @old_stdout, $stdout = $stdout, io
33
+ begin; yield ensure; restore_stdout; end if block_given?
34
+ end
35
+
36
+ def restore_stdout
37
+ $stdout = @old_stdout
38
+ end
39
+
40
+ it 'should use the current directory to run in' do
41
+ mock_instance = mock('RunnerInstance')
42
+ mock_instance.stub!(:start)
43
+ RSpactor::Runner.should_receive(:new).with(Dir.pwd, {}).and_return(mock_instance)
44
+ RSpactor::Runner.start
45
+ end
46
+
47
+ it 'should take an optional directory to run in' do
48
+ mock_instance = mock('RunnerInstance')
49
+ mock_instance.stub!(:start)
50
+ RSpactor::Runner.should_receive(:new).with('/tmp/mu', {}).and_return(mock_instance)
51
+ RSpactor::Runner.start(:run_in => '/tmp/mu')
52
+ end
53
+
54
+ describe "start" do
55
+ before(:each) do
56
+ @runner = described_class.new('/my/path')
57
+ capture_stdout
58
+ end
59
+
60
+ after(:each) do
61
+ restore_stdout
62
+ end
63
+
64
+ def setup
65
+ @runner.start
66
+ end
67
+
68
+ context "Interactor" do
69
+ before(:each) do
70
+ @runner.stub!(:load_dotfile)
71
+ @runner.stub!(:start_listener)
72
+ @interactor = mock('Interactor')
73
+ @interactor.should_receive(:start_termination_handler)
74
+ RSpactor::Interactor.should_receive(:new).and_return(@interactor)
75
+ end
76
+
77
+ it "should start Interactor" do
78
+ @interactor.should_receive(:wait_for_enter_key).with(instance_of(String), 3)
79
+ setup
80
+ end
81
+
82
+ it "should run all specs if Interactor isn't interrupted" do
83
+ @interactor.should_receive(:wait_for_enter_key).and_return(nil)
84
+ @runner.should_receive(:run_test_command).with([])
85
+ setup
86
+ end
87
+
88
+ it "should skip running all specs if Interactor is interrupted" do
89
+ @interactor.should_receive(:wait_for_enter_key).and_return(true)
90
+ @runner.should_not_receive(:run_spec_command)
91
+ setup
92
+ end
93
+ end
94
+
95
+ it "should initialize RSpecInspector" do
96
+ @runner.stub!(:load_dotfile)
97
+ @runner.stub!(:start_interactor)
98
+ RSpactor::TestInspector.should_receive(:new).with('/my/path')
99
+ RSpactor::Listener.stub!(:new).and_return(mock('Listener').as_null_object)
100
+ setup
101
+ end
102
+
103
+ context "Listener" do
104
+ before(:each) do
105
+ @runner.stub!(:load_dotfile)
106
+ @runner.stub!(:start_interactor)
107
+ @inspector = mock("Inspector")
108
+ RSpactor::TestInspector.stub!(:new).and_return(@inspector)
109
+ @listener = mock('Listener')
110
+ end
111
+
112
+ it "should run Listener" do
113
+ @listener.should_receive(:run).with('/my/path')
114
+ RSpactor::Listener.should_receive(:new).with(instance_of(Array)).and_return(@listener)
115
+ setup
116
+ end
117
+ end
118
+
119
+ it "should output 'watching' message on start" do
120
+ @runner.stub!(:load_dotfile)
121
+ @runner.stub!(:start_interactor)
122
+ @runner.stub!(:start_listener)
123
+ setup
124
+ $stdout.string.chomp.should == "** RSpactor is now watching at '/my/path'"
125
+ end
126
+
127
+ context "dotfile" do
128
+ before(:each) do
129
+ @runner.stub!(:start_interactor)
130
+ @runner.stub!(:start_listener)
131
+ end
132
+
133
+ it "should load dotfile if found" do
134
+ with_env('HOME', '/home/moo') do
135
+ File.should_receive(:exists?).with('/home/moo/.rspactor').and_return(true)
136
+ Kernel.should_receive(:load).with('/home/moo/.rspactor')
137
+ setup
138
+ end
139
+ end
140
+
141
+ it "should continue even if the dotfile raised errors" do
142
+ with_env('HOME', '/home/moo') do
143
+ File.should_receive(:exists?).and_return(true)
144
+ Kernel.should_receive(:load).with('/home/moo/.rspactor').and_raise(ArgumentError)
145
+ capture_stderr do
146
+ lambda { setup }.should_not raise_error
147
+ $stderr.string.split("\n").should include('Error while loading /home/moo/.rspactor: ArgumentError')
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ describe "#run_spec_command" do
155
+ before(:each) do
156
+ @runner = described_class.new('/my/path')
157
+ end
158
+
159
+ def with_rubyopt(string, &block)
160
+ with_env('RUBYOPT', string, &block)
161
+ end
162
+
163
+ def run(paths)
164
+ @runner.run_spec_command(paths)
165
+ end
166
+
167
+ it "should exit if the paths argument is empty" do
168
+ @runner.should_not_receive(:run_command)
169
+ run([])
170
+ end
171
+
172
+ it "should specify runner spec runner with joined paths" do
173
+ run(%w(foo bar)).should include(' spec foo bar ')
174
+ end
175
+
176
+ it "should specify default options: --color" do
177
+ run('foo').should include(' --color')
178
+ end
179
+
180
+ it "should setup RUBYOPT environment variable" do
181
+ with_rubyopt(nil) do
182
+ run('foo').should include("RUBYOPT='-Ilib:test' ")
183
+ end
184
+ end
185
+
186
+ it "should concat existing RUBYOPTs" do
187
+ with_rubyopt('-rubygems -w') do
188
+ run('foo').should include("RUBYOPT='-Ilib:test -rubygems -w' ")
189
+ end
190
+ end
191
+
192
+ it "should include growl formatter" do
193
+ run('foo').should include(' -f RSpecGrowler:STDOUT')
194
+ end
195
+
196
+ it "should include 'progress' formatter" do
197
+ run('foo').should include(' -f progress')
198
+ end
199
+
200
+ it "should not include 'progress' formatter if there already are 2 or more formatters" do
201
+ @runner.should_receive(:formatter_opts).and_return('-f foo --format bar')
202
+ run('foo').should_not include('-f progress')
203
+ end
204
+
205
+ it "should save status of last run" do
206
+ @runner.should_receive(:run_command).twice.and_return(true, false)
207
+ run('foo')
208
+ @runner.last_run_failed?.should be_false
209
+ run('bar')
210
+ @runner.last_run_failed?.should be_true
211
+ run([])
212
+ @runner.last_run_failed?.should be_false
213
+ end
214
+ end
215
+
216
+ describe "#test_changed_files" do
217
+ before(:each) do
218
+ @runner = described_class.new('.')
219
+ @runner.stub!(:inspector).and_return(mock("Inspector"))
220
+ end
221
+
222
+ def set_inspector_expectation(file, ret)
223
+ @runner.inspector.should_receive(:determine_spec_files).with(file).and_return(ret)
224
+ end
225
+
226
+ it "should find and run spec files" do
227
+ set_inspector_expectation('moo.rb', ['spec/moo_spec.rb'])
228
+ set_inspector_expectation('views/baz.haml', [])
229
+ set_inspector_expectation('config/bar.yml', ['spec/bar_spec.rb', 'spec/bar_stuff_spec.rb'])
230
+
231
+ expected = %w(spec/moo_spec.rb spec/bar_spec.rb spec/bar_stuff_spec.rb)
232
+ @runner.should_receive(:run_spec_command).with(expected)
233
+
234
+ capture_stdout do
235
+ @runner.send(:test_changed_files, %w(moo.rb views/baz.haml config/bar.yml))
236
+ $stdout.string.split("\n").should == expected
237
+ end
238
+ end
239
+
240
+ it "should run the full suite after a run succeded when the previous one failed" do
241
+ @runner.inspector.stub!(:determine_spec_files).and_return(['spec/foo_spec.rb'], ['spec/bar_spec.rb'])
242
+ @runner.stub!(:options).and_return({ :retry_failed => true })
243
+
244
+ capture_stdout do
245
+ @runner.stub!(:run_spec_command)
246
+ @runner.should_receive(:last_run_failed?).and_return(true, false)
247
+ @runner.should_receive(:run_all_tests)
248
+ @runner.send(:test_changed_files, %w(moo.rb))
249
+ end
250
+ end
251
+ end
252
+
253
+ it "should have Runner in global namespace for backwards compatibility" do
254
+ defined?(::Runner).should be_true
255
+ ::Runner.should == described_class
256
+ end
257
+
258
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lacomartincik-rspactor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.3.1
5
+ platform: ruby
6
+ authors:
7
+ - "Mislav Marohni\xC4\x87"
8
+ - Andreas Wolff
9
+ - Pelle Braendgaard
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-05-11 00:00:00 -07:00
15
+ default_executable:
16
+ dependencies: []
17
+
18
+ description:
19
+ email: mislav.marohnic@gmail.com
20
+ executables:
21
+ - rspactor
22
+ - rspactor-system
23
+ extensions: []
24
+
25
+ extra_rdoc_files: []
26
+
27
+ files:
28
+ - Rakefile
29
+ - bin/rspactor
30
+ - bin/rspactor-system
31
+ - lib/rspactor
32
+ - lib/rspactor/growl.rb
33
+ - lib/rspactor/interactor.rb
34
+ - lib/rspactor/listener.rb
35
+ - lib/rspactor/rspec_inspector.rb
36
+ - lib/rspactor/runner.rb
37
+ - lib/rspactor/test_inspector.rb
38
+ - lib/rspactor.rb
39
+ - lib/rspec_growler.rb
40
+ - images/failed.png
41
+ - images/pending.png
42
+ - images/success.png
43
+ - spec/listener_spec.rb
44
+ - spec/rspec_inspector_spec.rb
45
+ - spec/runner_spec.rb
46
+ - LICENSE
47
+ has_rdoc: false
48
+ homepage: http://github.com/mislav/rspactor
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.2.0
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: RSpactor is a command line tool to automatically run your changed specs (much like autotest).
73
+ test_files: []
74
+