mislav-rspactor 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +38 -0
- data/bin/rspactor +2 -2
- data/images/failed.png +0 -0
- data/images/pending.png +0 -0
- data/images/success.png +0 -0
- data/lib/rspactor.rb +6 -4
- data/lib/rspactor/inspector.rb +84 -0
- data/lib/rspactor/interactor.rb +63 -0
- data/lib/rspactor/listener.rb +86 -0
- data/lib/rspactor/runner.rb +84 -0
- data/lib/rspec_growler.rb +27 -0
- data/spec/inspector_spec.rb +3 -3
- data/spec/listener_spec.rb +8 -4
- data/spec/runner_spec.rb +32 -27
- metadata +16 -13
- data/VERSION.yml +0 -4
- data/lib/inspector.rb +0 -84
- data/lib/interactor.rb +0 -63
- data/lib/listener.rb +0 -86
- data/lib/resulting.rb +0 -47
- data/lib/runner.rb +0 -82
data/Rakefile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'jeweler'
|
|
3
|
+
|
|
4
|
+
Jeweler.class_eval do
|
|
5
|
+
protected
|
|
6
|
+
|
|
7
|
+
def fill_in_gemspec_defaults(gemspec)
|
|
8
|
+
if gemspec.files.nil? || gemspec.files.empty?
|
|
9
|
+
gemspec.files = FileList['Rakefile', '{bin,lib,images,spec}/**/*', 'README*', 'LICENSE*']
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
if gemspec.executables.nil? || gemspec.executables.empty?
|
|
13
|
+
gemspec.executables = Dir["#{@base_dir}/bin/*"].map do |f|
|
|
14
|
+
File.basename(f)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
gemspec
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Jeweler::Tasks.new do |gem|
|
|
23
|
+
gem.name = "rspactor"
|
|
24
|
+
gem.summary = "RSpactor is a command line tool to automatically run your changed specs (much like autotest)."
|
|
25
|
+
gem.email = "mislav.marohnic@gmail.com"
|
|
26
|
+
gem.homepage = "http://github.com/mislav/rspactor"
|
|
27
|
+
gem.authors = ["Mislav Marohnić", "Andreas Wolff", "Pelle Braendgaard"]
|
|
28
|
+
gem.has_rdoc = false
|
|
29
|
+
end
|
|
30
|
+
rescue LoadError
|
|
31
|
+
puts "Jeweler gem (technicalpickles-jeweler) not found"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
task :spec do
|
|
35
|
+
system 'ruby -Ilib bin/rspactor'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
task :default => :spec
|
data/bin/rspactor
CHANGED
data/images/failed.png
ADDED
|
Binary file
|
data/images/pending.png
ADDED
|
Binary file
|
data/images/success.png
ADDED
|
Binary file
|
data/lib/rspactor.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
module RSpactor
|
|
2
|
+
autoload :Interactor, 'rspactor/interactor'
|
|
3
|
+
autoload :Listener, 'rspactor/listener'
|
|
4
|
+
autoload :Inspector, 'rspactor/inspector'
|
|
5
|
+
autoload :Runner, 'rspactor/runner'
|
|
6
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
spec_files = candidates.select { |candidate| File.exists? candidate }
|
|
14
|
+
|
|
15
|
+
if spec_files.empty?
|
|
16
|
+
$stderr.puts "doesn't exist: #{candidates.inspect}"
|
|
17
|
+
end
|
|
18
|
+
spec_files
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# mappings for Rails are inspired by autotest mappings in rspec-rails
|
|
22
|
+
def translate(file)
|
|
23
|
+
file = file.sub(%r:^#{Regexp.escape(@root)}/:, '')
|
|
24
|
+
candidates = []
|
|
25
|
+
|
|
26
|
+
if spec_file?(file)
|
|
27
|
+
candidates << file
|
|
28
|
+
else
|
|
29
|
+
spec_file = append_spec_file_extension(file)
|
|
30
|
+
|
|
31
|
+
case file
|
|
32
|
+
when %r:^app/:
|
|
33
|
+
if file =~ %r:^app/controllers/application(_controller)?.rb$:
|
|
34
|
+
candidates << 'controllers'
|
|
35
|
+
elsif file == 'app/helpers/application_helper.rb'
|
|
36
|
+
candidates << 'helpers' << 'views'
|
|
37
|
+
else
|
|
38
|
+
candidates << spec_file.sub('app/', '')
|
|
39
|
+
|
|
40
|
+
if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
|
|
41
|
+
candidates << append_spec_file_extension($1)
|
|
42
|
+
elsif file =~ %r:app/helpers/(\w+)_helper.rb:
|
|
43
|
+
candidates << "views/#{$1}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
when %r:^lib/:
|
|
47
|
+
candidates << spec_file
|
|
48
|
+
# lib/foo/bar_spec.rb -> lib/bar_spec.rb
|
|
49
|
+
candidates << candidates.last.sub($&, '')
|
|
50
|
+
# lib/bar_spec.rb -> bar_spec.rb
|
|
51
|
+
candidates << candidates.last.sub(%r:\w+/:, '') if candidates.last.index('/')
|
|
52
|
+
when 'config/routes.rb'
|
|
53
|
+
candidates << 'controllers' << 'helpers' << 'views'
|
|
54
|
+
when 'config/database.yml', 'db/schema.rb'
|
|
55
|
+
candidates << 'models'
|
|
56
|
+
when %r:^(spec/(spec_helper|shared/.*)|config/(boot|environment(s/test)?))\.rb$:, 'spec/spec.opts'
|
|
57
|
+
candidates << 'spec'
|
|
58
|
+
else
|
|
59
|
+
candidates << spec_file
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
candidates.map do |candidate|
|
|
64
|
+
if candidate.index('spec') == 0
|
|
65
|
+
candidate
|
|
66
|
+
else
|
|
67
|
+
'spec/' + candidate
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def append_spec_file_extension(file)
|
|
73
|
+
if File.extname(file) == ".rb"
|
|
74
|
+
file.sub(/.rb$/, "_spec.rb")
|
|
75
|
+
else
|
|
76
|
+
file + "_spec.rb"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def spec_file?(file)
|
|
81
|
+
file =~ /^spec\/.+_spec.rb$/
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
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,84 @@
|
|
|
1
|
+
require 'rspactor'
|
|
2
|
+
|
|
3
|
+
module RSpactor
|
|
4
|
+
class Runner
|
|
5
|
+
def self.load
|
|
6
|
+
dotfile = File.join(ENV['HOME'], '.rspactor')
|
|
7
|
+
Kernel.load dotfile if File.exists?(dotfile)
|
|
8
|
+
|
|
9
|
+
dir = Dir.pwd
|
|
10
|
+
@inspector = Inspector.new(dir)
|
|
11
|
+
@interactor = Interactor.new
|
|
12
|
+
|
|
13
|
+
puts "** RSpactor is now watching at '#{dir}'"
|
|
14
|
+
|
|
15
|
+
aborted = initial_spec_run_abort
|
|
16
|
+
@interactor.start_termination_handler
|
|
17
|
+
run_all_specs unless aborted
|
|
18
|
+
|
|
19
|
+
Listener.new(Inspector::EXTENSIONS) do |files|
|
|
20
|
+
files_to_spec = []
|
|
21
|
+
files.each do |file|
|
|
22
|
+
spec_files = @inspector.determine_spec_files(file)
|
|
23
|
+
unless spec_files.empty?
|
|
24
|
+
puts spec_files.join("\n")
|
|
25
|
+
files_to_spec.concat spec_files
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
run_spec_command(files_to_spec)
|
|
29
|
+
end.run(dir)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.initial_spec_run_abort
|
|
33
|
+
@interactor.wait_for_enter_key("** Hit <enter> to skip initial spec run", 3)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.run_all_specs
|
|
37
|
+
run_spec_command('spec')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.run_spec_command(paths)
|
|
41
|
+
paths = Array(paths)
|
|
42
|
+
return if paths.empty?
|
|
43
|
+
run_command [ruby_opts, spec_runner, paths, spec_opts].flatten.join(' ')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.run_command(cmd)
|
|
47
|
+
system(cmd)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.spec_opts
|
|
51
|
+
if File.exist?('spec/spec.opts')
|
|
52
|
+
opts = File.read('spec/spec.opts').gsub("\n", ' ')
|
|
53
|
+
else
|
|
54
|
+
opts = "--color"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts << ' ' << formatter_opts
|
|
58
|
+
# only add the "progress" formatter unless no other (besides growl) is specified
|
|
59
|
+
opts << ' -f progress' unless opts.scan(/\s(?:-f|--format)\b/).length > 1
|
|
60
|
+
|
|
61
|
+
opts
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.formatter_opts
|
|
65
|
+
"-r #{File.dirname(__FILE__)}/../rspec_growler.rb -f RSpecGrowler:STDOUT"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.spec_runner
|
|
69
|
+
if File.exist?("script/spec")
|
|
70
|
+
"script/spec"
|
|
71
|
+
else
|
|
72
|
+
"spec"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.ruby_opts
|
|
77
|
+
other = ENV['RUBYOPT'] ? " #{ENV['RUBYOPT']}" : ''
|
|
78
|
+
%(RUBYOPT='-Ilib:spec#{other}')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# backward compatibility
|
|
84
|
+
Runner = RSpactor::Runner
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'spec/runner/formatter/base_formatter'
|
|
2
|
+
|
|
3
|
+
class RSpecGrowler < Spec::Runner::Formatter::BaseFormatter
|
|
4
|
+
def dump_summary(duration, total, failures, pending)
|
|
5
|
+
icon = if failures > 0
|
|
6
|
+
'failed'
|
|
7
|
+
elsif pending > 0
|
|
8
|
+
'pending'
|
|
9
|
+
else
|
|
10
|
+
'success'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
image_path = File.dirname(__FILE__) + "/../images/#{icon}.png"
|
|
14
|
+
message = "#{total} examples, #{failures} failures"
|
|
15
|
+
|
|
16
|
+
if pending > 0
|
|
17
|
+
message << " (#{pending} pending)"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
growl "Test Results", message, image_path, 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def growl(title, msg, img, pri = 0)
|
|
24
|
+
system("growlnotify -w -n rspactor --image #{img} -p #{pri} -m #{msg.inspect} #{title} &")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
data/spec/inspector_spec.rb
CHANGED
data/spec/listener_spec.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
require 'listener'
|
|
1
|
+
require 'rspactor/listener'
|
|
2
2
|
|
|
3
|
-
describe Listener do
|
|
3
|
+
describe RSpactor::Listener do
|
|
4
4
|
before(:all) do
|
|
5
|
-
@listener =
|
|
5
|
+
@listener = described_class.new(%w(rb erb haml))
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
it "should be timestamped" do
|
|
@@ -21,8 +21,12 @@ describe Listener do
|
|
|
21
21
|
@listener.ignore_file?('/project/.foo').should be
|
|
22
22
|
end
|
|
23
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
|
+
|
|
24
28
|
it "should not ignore files without extension" do
|
|
25
|
-
@listener.ignore_file?('/project/foo.rb').
|
|
29
|
+
@listener.ignore_file?('/project/foo.rb').should be_false
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
it "should ignore files without extension" do
|
data/spec/runner_spec.rb
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
require 'runner'
|
|
1
|
+
require 'rspactor/runner'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
cmd
|
|
3
|
+
describe RSpactor::Runner do
|
|
4
|
+
|
|
5
|
+
described_class.class_eval do
|
|
6
|
+
def self.run_command(cmd)
|
|
7
|
+
# never shell out in tests
|
|
8
|
+
cmd
|
|
9
|
+
end
|
|
7
10
|
end
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
describe ::Runner do
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
def runner
|
|
13
|
+
described_class
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def with_env(name, value)
|
|
@@ -27,18 +27,18 @@ describe ::Runner do
|
|
|
27
27
|
before(:each) do
|
|
28
28
|
Dir.stub!(:pwd).and_return('/my/path')
|
|
29
29
|
File.stub!(:exists?).and_return(false)
|
|
30
|
-
|
|
31
|
-
Inspector.stub!(:new)
|
|
32
|
-
Interactor.stub!(:new).and_return(mock('Interactor').as_null_object)
|
|
33
|
-
Listener.stub!(:new).and_return(mock('Listener').as_null_object)
|
|
30
|
+
runner.stub!(:puts)
|
|
31
|
+
RSpactor::Inspector.stub!(:new)
|
|
32
|
+
RSpactor::Interactor.stub!(:new).and_return(mock('Interactor').as_null_object)
|
|
33
|
+
RSpactor::Listener.stub!(:new).and_return(mock('Listener').as_null_object)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def setup
|
|
37
|
-
|
|
37
|
+
runner.load
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
it "should initialize Inspector" do
|
|
41
|
-
Inspector.should_receive(:new).with('/my/path')
|
|
41
|
+
RSpactor::Inspector.should_receive(:new).with('/my/path')
|
|
42
42
|
setup
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -46,35 +46,35 @@ describe ::Runner do
|
|
|
46
46
|
interactor = mock('Interactor')
|
|
47
47
|
interactor.should_receive(:wait_for_enter_key).with(instance_of(String), 3)
|
|
48
48
|
interactor.should_receive(:start_termination_handler)
|
|
49
|
-
Interactor.should_receive(:new).and_return(interactor)
|
|
49
|
+
RSpactor::Interactor.should_receive(:new).and_return(interactor)
|
|
50
50
|
setup
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
it "should run all specs if Interactor isn't interrupted" do
|
|
54
54
|
interactor = mock('Interactor', :start_termination_handler => nil)
|
|
55
55
|
interactor.should_receive(:wait_for_enter_key).and_return(nil)
|
|
56
|
-
Interactor.should_receive(:new).and_return(interactor)
|
|
57
|
-
|
|
56
|
+
RSpactor::Interactor.should_receive(:new).and_return(interactor)
|
|
57
|
+
runner.should_receive(:run_spec_command).with('spec')
|
|
58
58
|
setup
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
it "should skip running all specs if Interactor is interrupted" do
|
|
62
62
|
interactor = mock('Interactor', :start_termination_handler => nil)
|
|
63
63
|
interactor.should_receive(:wait_for_enter_key).and_return(true)
|
|
64
|
-
Interactor.should_receive(:new).and_return(interactor)
|
|
65
|
-
|
|
64
|
+
RSpactor::Interactor.should_receive(:new).and_return(interactor)
|
|
65
|
+
runner.should_not_receive(:run_spec_command)
|
|
66
66
|
setup
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
it "should run Listener" do
|
|
70
70
|
listener = mock('Listener')
|
|
71
71
|
listener.should_receive(:run).with('/my/path')
|
|
72
|
-
Listener.should_receive(:new).with(instance_of(Array)).and_return(listener)
|
|
72
|
+
RSpactor::Listener.should_receive(:new).with(instance_of(Array)).and_return(listener)
|
|
73
73
|
setup
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
it "should output 'watching' message on start" do
|
|
77
|
-
|
|
77
|
+
runner.should_receive(:puts).with("** RSpactor is now watching at '/my/path'")
|
|
78
78
|
setup
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -93,11 +93,11 @@ describe ::Runner do
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def run(paths)
|
|
96
|
-
|
|
96
|
+
runner.run_spec_command(paths)
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
it "should exit if the paths argument is empty" do
|
|
100
|
-
|
|
100
|
+
runner.should_not_receive(:run_command)
|
|
101
101
|
run([])
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -122,7 +122,7 @@ describe ::Runner do
|
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
it "should include growl formatter" do
|
|
125
|
-
run('foo').should include(' -f
|
|
125
|
+
run('foo').should include(' -f RSpecGrowler:STDOUT')
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
it "should include 'progress' formatter" do
|
|
@@ -130,9 +130,14 @@ describe ::Runner do
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
it "should not include 'progress' formatter if there already are 2 or more formatters" do
|
|
133
|
-
|
|
133
|
+
runner.should_receive(:formatter_opts).and_return('-f foo --format bar')
|
|
134
134
|
run('foo').should_not include('-f progress')
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
it "should have Runner in global namespace for backwards compatibility" do
|
|
139
|
+
defined?(::Runner).should be_true
|
|
140
|
+
::Runner.should == runner
|
|
141
|
+
end
|
|
142
|
+
|
|
138
143
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mislav-rspactor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- "Mislav Marohni\xC4\x87"
|
|
@@ -21,27 +21,30 @@ executables:
|
|
|
21
21
|
- rspactor
|
|
22
22
|
extensions: []
|
|
23
23
|
|
|
24
|
-
extra_rdoc_files:
|
|
25
|
-
|
|
24
|
+
extra_rdoc_files: []
|
|
25
|
+
|
|
26
26
|
files:
|
|
27
|
-
-
|
|
27
|
+
- Rakefile
|
|
28
28
|
- bin/rspactor
|
|
29
|
-
- lib/
|
|
30
|
-
- lib/
|
|
31
|
-
- lib/
|
|
32
|
-
- lib/
|
|
29
|
+
- lib/rspactor
|
|
30
|
+
- lib/rspactor/inspector.rb
|
|
31
|
+
- lib/rspactor/interactor.rb
|
|
32
|
+
- lib/rspactor/listener.rb
|
|
33
|
+
- lib/rspactor/runner.rb
|
|
33
34
|
- lib/rspactor.rb
|
|
34
|
-
- lib/
|
|
35
|
+
- lib/rspec_growler.rb
|
|
36
|
+
- images/failed.png
|
|
37
|
+
- images/pending.png
|
|
38
|
+
- images/success.png
|
|
35
39
|
- spec/inspector_spec.rb
|
|
36
40
|
- spec/listener_spec.rb
|
|
37
41
|
- spec/runner_spec.rb
|
|
38
42
|
- LICENSE
|
|
39
|
-
has_rdoc:
|
|
43
|
+
has_rdoc: false
|
|
40
44
|
homepage: http://github.com/mislav/rspactor
|
|
41
45
|
post_install_message:
|
|
42
|
-
rdoc_options:
|
|
43
|
-
|
|
44
|
-
- --charset=UTF-8
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
|
|
45
48
|
require_paths:
|
|
46
49
|
- lib
|
|
47
50
|
required_ruby_version: !ruby/object:Gem::Requirement
|
data/VERSION.yml
DELETED
data/lib/inspector.rb
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# Maps the changed filenames to list of specs to run in the next go.
|
|
2
|
-
# Assumes Rails-like directory structure
|
|
3
|
-
class Inspector
|
|
4
|
-
|
|
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
|
-
spec_files = candidates.select { |candidate| File.exists? candidate }
|
|
14
|
-
|
|
15
|
-
if spec_files.empty?
|
|
16
|
-
$stderr.puts "doesn't exist: #{candidates.inspect}"
|
|
17
|
-
end
|
|
18
|
-
spec_files
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# mappings for Rails are inspired by autotest mappings in rspec-rails
|
|
22
|
-
def translate(file)
|
|
23
|
-
file = file.sub(%r:^#{Regexp.escape(@root)}/:, '')
|
|
24
|
-
candidates = []
|
|
25
|
-
|
|
26
|
-
if spec_file?(file)
|
|
27
|
-
candidates << file
|
|
28
|
-
else
|
|
29
|
-
spec_file = append_spec_file_extension(file)
|
|
30
|
-
|
|
31
|
-
case file
|
|
32
|
-
when %r:^app/:
|
|
33
|
-
if file =~ %r:^app/controllers/application(_controller)?.rb$:
|
|
34
|
-
candidates << 'controllers'
|
|
35
|
-
elsif file == 'app/helpers/application_helper.rb'
|
|
36
|
-
candidates << 'helpers' << 'views'
|
|
37
|
-
else
|
|
38
|
-
candidates << spec_file.sub('app/', '')
|
|
39
|
-
|
|
40
|
-
if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
|
|
41
|
-
candidates << append_spec_file_extension($1)
|
|
42
|
-
elsif file =~ %r:app/helpers/(\w+)_helper.rb:
|
|
43
|
-
candidates << "views/#{$1}"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
when %r:^lib/:
|
|
47
|
-
candidates << spec_file
|
|
48
|
-
# lib/foo/bar_spec.rb -> lib/bar_spec.rb
|
|
49
|
-
candidates << candidates.last.sub($&, '')
|
|
50
|
-
# lib/bar_spec.rb -> bar_spec.rb
|
|
51
|
-
candidates << candidates.last.sub(%r:\w+/:, '') if candidates.last.index('/')
|
|
52
|
-
when 'config/routes.rb'
|
|
53
|
-
candidates << 'controllers' << 'helpers' << 'views'
|
|
54
|
-
when 'config/database.yml', 'db/schema.rb'
|
|
55
|
-
candidates << 'models'
|
|
56
|
-
when %r:^(spec/(spec_helper|shared/.*)|config/(boot|environment(s/test)?))\.rb$:, 'spec/spec.opts'
|
|
57
|
-
candidates << 'spec'
|
|
58
|
-
else
|
|
59
|
-
candidates << spec_file
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
candidates.map do |candidate|
|
|
64
|
-
if candidate.index('spec') == 0
|
|
65
|
-
candidate
|
|
66
|
-
else
|
|
67
|
-
'spec/' + candidate
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def append_spec_file_extension(file)
|
|
73
|
-
if File.extname(file) == ".rb"
|
|
74
|
-
file.sub(/.rb$/, "_spec.rb")
|
|
75
|
-
else
|
|
76
|
-
file + "_spec.rb"
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def spec_file?(file)
|
|
81
|
-
file =~ /^spec\/.+_spec.rb$/
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
end
|
data/lib/interactor.rb
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
require 'timeout'
|
|
2
|
-
|
|
3
|
-
class Interactor
|
|
4
|
-
|
|
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
|
-
|
|
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
|
data/lib/listener.rb
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
require 'osx/foundation'
|
|
2
|
-
OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
|
|
3
|
-
|
|
4
|
-
# Some code borrowed from http://rails.aizatto.com/2007/11/28/taming-the-autotest-beast-with-fsevents/
|
|
5
|
-
class Listener
|
|
6
|
-
|
|
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
|
-
|
|
86
|
-
end
|
data/lib/resulting.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
class RSpactorFormatter
|
|
2
|
-
attr_accessor :example_group, :options, :where
|
|
3
|
-
def initialize(options, where)
|
|
4
|
-
@options = options
|
|
5
|
-
@where = where
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def dump_summary(duration, example_count, failure_count, pending_count)
|
|
9
|
-
img = (failure_count == 0) ? "rails_ok.png" : "rails_fail.png"
|
|
10
|
-
growl "Test Results", "#{example_count} examples, #{failure_count} failures", File.dirname(__FILE__) + "/../asset/#{img}", 0
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def start(*ignore_these)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def add_example_group(*ignore_these)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def example_started(*ignore_these)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def example_passed(*ignore_these)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def example_failed(*ignore_these)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def example_pending( *ignore_these)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def start_dump
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def dump_failure(*ignore_these)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def dump_pending
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def close
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def growl(title, msg, img, pri = 0)
|
|
44
|
-
system("growlnotify -w -n rspactor --image #{img} -p #{pri} -m #{msg.inspect} #{title} &")
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
data/lib/runner.rb
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
require 'inspector'
|
|
2
|
-
require 'interactor'
|
|
3
|
-
require 'listener'
|
|
4
|
-
|
|
5
|
-
class Runner
|
|
6
|
-
|
|
7
|
-
def self.load
|
|
8
|
-
dotfile = File.join(ENV['HOME'], '.rspactor')
|
|
9
|
-
Kernel.load dotfile if File.exists?(dotfile)
|
|
10
|
-
|
|
11
|
-
dir = Dir.pwd
|
|
12
|
-
@inspector = Inspector.new(dir)
|
|
13
|
-
@interactor = Interactor.new
|
|
14
|
-
|
|
15
|
-
puts "** RSpactor is now watching at '#{dir}'"
|
|
16
|
-
|
|
17
|
-
aborted = initial_spec_run_abort
|
|
18
|
-
@interactor.start_termination_handler
|
|
19
|
-
run_all_specs unless aborted
|
|
20
|
-
|
|
21
|
-
Listener.new(Inspector::EXTENSIONS) do |files|
|
|
22
|
-
files_to_spec = []
|
|
23
|
-
files.each do |file|
|
|
24
|
-
spec_files = @inspector.determine_spec_files(file)
|
|
25
|
-
unless spec_files.empty?
|
|
26
|
-
puts spec_files.join("\n")
|
|
27
|
-
files_to_spec.concat spec_files
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
run_spec_command(files_to_spec)
|
|
31
|
-
end.run(dir)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.initial_spec_run_abort
|
|
35
|
-
@interactor.wait_for_enter_key("** Hit <enter> to skip initial spec run", 3)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.run_all_specs
|
|
39
|
-
run_spec_command('spec')
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.run_spec_command(paths)
|
|
43
|
-
paths = Array(paths)
|
|
44
|
-
return if paths.empty?
|
|
45
|
-
run_command [ruby_opts, spec_runner, paths, spec_opts].flatten.join(' ')
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.run_command(cmd)
|
|
49
|
-
system(cmd)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.spec_opts
|
|
53
|
-
if File.exist?('spec/spec.opts')
|
|
54
|
-
opts = File.read('spec/spec.opts').gsub("\n", ' ')
|
|
55
|
-
else
|
|
56
|
-
opts = "--color"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
opts << ' ' << formatter_opts
|
|
60
|
-
# only add the "progress" formatter unless no other (besides growl) is specified
|
|
61
|
-
opts << ' -f progress' unless opts.scan(/\s(?:-f|--format)\b/).length > 1
|
|
62
|
-
|
|
63
|
-
opts
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.formatter_opts
|
|
67
|
-
"-r #{File.dirname(__FILE__)}/resulting.rb -f RSpactorFormatter:STDOUT"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def self.spec_runner
|
|
71
|
-
if File.exist?("script/spec")
|
|
72
|
-
"script/spec"
|
|
73
|
-
else
|
|
74
|
-
"spec"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def self.ruby_opts
|
|
79
|
-
other = ENV['RUBYOPT'] ? " #{ENV['RUBYOPT']}" : ''
|
|
80
|
-
%(RUBYOPT='-Ilib:spec#{other}')
|
|
81
|
-
end
|
|
82
|
-
end
|