rspactor 0.6.4 → 0.7.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Mislav Marohnić
1
+ Copyright (c) 2010 Thibaud Guillaume-Gentil
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
17
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
18
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
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.
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc CHANGED
@@ -1,23 +1,70 @@
1
- === Dependencies
1
+ = RSpactor
2
2
 
3
- RubyCocoa is needed (you can help to switch to ruby-fsevent, see branches)
3
+ RSpactor allows to automatically & intelligently launch your specs when your files are modified.
4
+ Version 0.7.x is a complete rewrite, RubyCocoa is no more needed, FSEvents are supported from scratch.
4
5
 
5
- Installing ruby-cocoa with rvm: http://gist.github.com/294465
6
+ == Features
6
7
 
7
- === Install
8
+ * FSEvent support (without RubyCocoa!)
9
+ * RSpec 2.0 support (from beta.14)
10
+ * Bundler support
11
+ * Super fast change detection
12
+ * Automatic _spec.rb files detection (even new file created, unlike watchr)
13
+ * Growl notification (please install {growlnotify}[http://growl.info/documentation/growlnotify.php])
8
14
 
9
- gem install rspactor
15
+ == Install
10
16
 
11
- === Usage
17
+ At the moment, only Mac OS X (10.5+) is supported. Tested on ruby 1.8.7 & 1.9.2dev.
12
18
 
13
- cd /path/to/rails/root/
14
- rspactor
19
+ Install the gem:
15
20
 
16
- === Command Line Options
21
+ gem install rspactor
17
22
 
18
- * <code> --coral </code> use coral
19
- * <code> --drb </code> use spork
20
- * <code> --view </code> run view specs
21
- * <code> --clear </code> clear before each run
22
- * <code> --skip </code> skip initial test run
23
- * <code> --run_in </code>
23
+ If you are using Bundler, please add it to your Gemfile (inside test group):
24
+
25
+ gem 'rspactor', '>= 0.7.beta.1'
26
+
27
+ == Usage
28
+
29
+ Just launch RSpactor inside your ruby/rails project with:
30
+
31
+ rspactor
32
+
33
+ Options list is available with:
34
+
35
+ rspactor -h
36
+
37
+ Signal handlers are now used to interact with RSpactor:
38
+
39
+ * Ctrl-C => Quit RSpactor or quick abort running spec(s)
40
+ * Ctrl-/ => Running all specs
41
+
42
+ == TODO
43
+
44
+ * Specific files (spec_helper, factories, fixtures...) inspections
45
+ * RSpec 1.3 support
46
+ * Inotify support (linux)
47
+ * Spork support (when {this issue}[http://github.com/timcharper/spork/issues#issue/37] will be resolved)
48
+ * Cucumber support (if really needed? {Steak}[http://github.com/cavalle/steak] works fine.)
49
+ * Other ideas?
50
+
51
+ == Development
52
+
53
+ * Source hosted at {GitHub}[http://github.com/thibaudgg/rspactor].
54
+ * Report issues/Questions/Feature requests on {GitHub Issues}[http://github.com/thibaudgg/rspactor/issues]
55
+
56
+ Pull requests are very welcome! Make sure your patches are well tested. Please create a topic branch for every separate change
57
+ you make.
58
+
59
+ == Authors
60
+
61
+ From version 0.7.x was completely rewritten by {Thibaud Guillaume-Gentil}[http://github.com/thibaudgg].
62
+
63
+ Older versions authors are:
64
+
65
+ * {Mislav Marohnić}[http://github.com/mislav]
66
+ * {Andreas Wolff}[http://github.com/phunkwork]
67
+ * {Pelle Braendgaard}[http://github.com/pelle]
68
+ * {Thibaud Guillaume-Gentil}[http://github.com/thibaudgg]
69
+
70
+ Thanks to {Giovanni Cangiani}[http://github.com/multiscan] for the IO.open/FSEvent trick & {Rémy Coutable}[http://github.com/rymai] for beta testing.
data/bin/rspactor CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
- require 'rspactor/runner'
2
+ require 'trollop'
3
+ require 'rspactor'
4
+ require 'rspactor/version'
3
5
 
4
- RSpactor::Runner.start({
5
- :coral => ARGV.delete('--coral'),
6
- :spork => ARGV.delete('--drb'),
7
- :view => ARGV.delete('--view'), # by default, rspactor didn't catch specs view
8
- :clear => ARGV.delete('--clear'),
9
- :skip => ARGV.delete('--skip'),
10
- :run_in => ARGV.last
11
- })
6
+ options = Trollop::options do
7
+ version RSpactor::VERSION
8
+
9
+ opt :clear, "Clear the console beetween each spec(s) run"
10
+ end
11
+
12
+ RSpactor.start(options)
@@ -0,0 +1,28 @@
1
+ # Workaround to make Rubygems believe it builds a native gem
2
+
3
+ def emulate_extension_install(extension_name)
4
+ File.open('Makefile', 'w') { |f| f.write "all:\n\ninstall:\n\n" }
5
+ File.open('make', 'w') do |f|
6
+ f.write '#!/bin/sh'
7
+ f.chmod f.stat.mode | 0111
8
+ end
9
+ File.open(extension_name + '.so', 'w') {}
10
+ File.open(extension_name + '.dll', 'w') {}
11
+ File.open('nmake.bat', 'w') { |f| }
12
+ end
13
+
14
+ emulate_extension_install('fsevent')
15
+
16
+ # Compile the actual fsevent_watch binary
17
+
18
+ raise "Only Darwin (Mac OS X) systems are supported for the moment" unless `uname -s`.chomp == 'Darwin'
19
+
20
+ GEM_ROOT = File.expand_path(File.join('..', '..'))
21
+ DARWIN_VERSION = `uname -r`.to_i
22
+ SDK_VERSION = { 9 => '10.5', 10 => '10.6', 11 => '10.7' }[DARWIN_VERSION]
23
+
24
+ raise "Darwin #{DARWIN_VERSION} is not supported" unless SDK_VERSION
25
+
26
+ `CFLAGS='-isysroot /Developer/SDKs/MacOSX#{SDK_VERSION}.sdk -mmacosx-version-min=#{SDK_VERSION}' /usr/bin/gcc -framework CoreServices -o "#{GEM_ROOT}/bin/fsevent_watch" fsevent_watch.c`
27
+
28
+ raise "Compilation of fsevent_watch failed (see README)" unless File.executable?("#{GEM_ROOT}/bin/fsevent_watch")
@@ -0,0 +1,44 @@
1
+ #include <CoreServices/CoreServices.h>
2
+
3
+ void callback(ConstFSEventStreamRef streamRef,
4
+ void *clientCallBackInfo,
5
+ size_t numEvents,
6
+ void *eventPaths,
7
+ const FSEventStreamEventFlags eventFlags[],
8
+ const FSEventStreamEventId eventIds[]
9
+ ) {
10
+ // Print modified dirs
11
+ int i;
12
+ char **paths = eventPaths;
13
+ for (i = 0; i < numEvents; i++) {
14
+ printf(paths[i]);
15
+ printf(" ");
16
+ }
17
+ printf("\n");
18
+ fflush(stdout);
19
+ }
20
+
21
+ int main (int argc, const char * argv[]) {
22
+ // Create event stream
23
+ CFStringRef pathToWatch = CFStringCreateWithCString(kCFAllocatorDefault, argv[1], kCFStringEncodingUTF8);
24
+ CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&pathToWatch, 1, NULL);
25
+ void *callbackInfo = NULL;
26
+ FSEventStreamRef stream;
27
+ CFAbsoluteTime latency = 0.5;
28
+ stream = FSEventStreamCreate(
29
+ kCFAllocatorDefault,
30
+ callback,
31
+ callbackInfo,
32
+ pathsToWatch,
33
+ kFSEventStreamEventIdSinceNow,
34
+ latency,
35
+ kFSEventStreamCreateFlagNone
36
+ );
37
+
38
+ // Add stream to run loop
39
+ FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
40
+ FSEventStreamStart(stream);
41
+ CFRunLoopRun();
42
+
43
+ return 2;
44
+ }
data/images/failed.png CHANGED
File without changes
data/images/success.png CHANGED
File without changes
@@ -0,0 +1,35 @@
1
+ require 'rspec/core/formatters/base_formatter'
2
+ require 'growl'
3
+
4
+ class GrowlFormatter < RSpec::Core::Formatters::ProgressFormatter
5
+
6
+ def dump_summary
7
+ super
8
+ failure_count = failed_examples.size
9
+ pending_count = pending_examples.size
10
+
11
+ icon = if failure_count > 0
12
+ 'failed'
13
+ elsif pending_count > 0
14
+ 'pending'
15
+ else
16
+ 'success'
17
+ end
18
+
19
+ message = "#{@example_count} examples, #{failure_count} failures"
20
+ if pending_count > 0
21
+ message << " (#{pending_count} pending)"
22
+ end
23
+ message << "\nin #{format_seconds(duration)} seconds"
24
+
25
+ Growl.notify message, :title => "RSpec results", :icon => image_path(icon)
26
+ end
27
+
28
+ private
29
+
30
+ # failed | pending | success
31
+ def image_path(icon)
32
+ File.expand_path(File.dirname(__FILE__) + "/../../images/#{icon}.png")
33
+ end
34
+
35
+ end
@@ -1,118 +1,52 @@
1
- require 'rspactor'
2
-
3
1
  module RSpactor
4
- # Maps the changed filenames to list of specs to run in the next go.
5
- # Assumes Rails-like directory structure
6
- class Inspector
7
- EXTENSIONS = %w(rb erb builder haml rhtml rxml yml conf opts feature)
8
-
9
- attr_reader :runner, :root
10
-
11
- def initialize(runner)
12
- @runner = runner
13
- @root = runner.dir
14
- end
15
-
16
- def determine_files(file)
17
- candidates = translate(file)
18
- cucumberable = candidates.delete('cucumber')
19
- runner.spork.reload if candidates.delete('spork') && runner.spork?
2
+ module Inspector
3
+ class << self
4
+ attr_reader :spec_paths
20
5
 
21
- candidates.reject { |candidate| candidate.index('.') }.each do |dir|
22
- candidates.reject! { |candidate| candidate.index("#{dir}/") == 0 }
6
+ def determine_spec_paths(files)
7
+ @spec_paths, @spec_files = [], nil
8
+ files.each { |file| translate(file) }
9
+ compact_spec_paths!
23
10
  end
24
- files = candidates.select { |candidate| File.exists? candidate }
25
11
 
26
- if files.empty? && !candidates.empty? && !cucumberable
27
- $stderr.puts "doesn't exist: #{candidates.inspect}"
12
+ def spec_paths?
13
+ @spec_paths.size > 0
28
14
  end
29
15
 
30
- files << 'cucumber' if cucumberable
31
- files
32
- end
33
-
34
- # mappings for Rails are inspired by autotest mappings in rspec-rails
35
- def translate(file)
36
- file = file.sub(%r:^#{Regexp.escape(root)}/:, '')
37
- candidates = []
16
+ private
38
17
 
39
- if spec_file?(file)
40
- candidates << file
41
- elsif cucumber_file?(file)
42
- candidates << 'cucumber'
43
- candidates << 'spork' if file =~ /^features\/support\//
44
- else
45
- spec_file = append_spec_file_extension(file)
46
-
47
- case file
48
- when %r:^app/:
49
- if file =~ %r:^app/controllers/application(_controller)?.rb$:
50
- candidates << 'controllers'
51
- elsif file == 'app/helpers/application_helper.rb'
52
- candidates << 'helpers'
53
- candidates << 'views' if runner.options[:view]
54
- elsif file.include?("app/views/")
55
- if runner.options[:view]
56
- candidates << spec_file.sub('app/', '')
57
- if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
58
- candidates << append_spec_file_extension($1)
59
- end
60
- end
61
- else
62
- candidates << spec_file.sub('app/', '')
63
- if file =~ %r:app/helpers/(\w+)_helper.rb:
64
- candidates << "views/#{$1}"
65
- elsif file =~ /_observer.rb$/
66
- candidates << candidates.last.sub('_observer', '')
67
- end
68
- end
69
- when %r:^lib/:
70
- candidates << spec_file
71
- # lib/foo/bar_spec.rb -> lib/bar_spec.rb
72
- candidates << candidates.last.sub($&, '')
73
- # lib/bar_spec.rb -> bar_spec.rb
74
- candidates << candidates.last.sub(%r:\w+/:, '') if candidates.last.index('/')
75
- when 'config/routes.rb'
76
- candidates << 'controllers' << 'helpers' << 'routing'
77
- candidates << 'views' if runner.options[:view]
78
- when 'config/database.yml', 'db/schema.rb', 'spec/factories.rb'
79
- candidates << 'models'
80
- when 'config/boot.rb', 'config/environment.rb', %r:^config/environments/:, %r:^config/initializers/:, %r:^vendor/:, 'spec/spec_helper.rb'
81
- candidates << 'spork'
82
- candidates << 'spec'
83
- when %r:^config/:
84
- # nothing
85
- when %r:^(spec/(spec_helper|shared/.*)|config/(boot|environment(s/test)?))\.rb$:, 'spec/spec.opts', 'spec/fakewebs.rb'
86
- candidates << 'spec'
18
+ def translate(file)
19
+ if spec_file?(file)
20
+ @spec_paths << file
87
21
  else
88
- candidates << spec_file
22
+ spec_file = append_spec_file_extension(file)
23
+ case file
24
+ when %r:^lib/:
25
+ @spec_paths << @spec_files.delete(spec_file.gsub(/^lib/, 'spec'))
26
+ @spec_paths << @spec_files.delete(spec_file.gsub(/^lib/, 'spec/lib'))
27
+ when %r:^app/:
28
+ @spec_paths << @spec_files.delete(spec_file.gsub(/^app/, 'spec'))
29
+ end
89
30
  end
90
31
  end
91
32
 
92
- candidates.map do |candidate|
93
- if candidate == 'cucumber' || candidate == 'spork'
94
- candidate
95
- elsif candidate.index('spec') == 0
96
- File.join(root, candidate)
97
- else
98
- File.join(root, 'spec', candidate)
99
- end
33
+ def compact_spec_paths!
34
+ @spec_paths.uniq!
35
+ @spec_paths.compact!
100
36
  end
101
- end
102
-
103
- def append_spec_file_extension(file)
104
- if File.extname(file) == ".rb"
105
- file.sub(/.rb$/, "_spec.rb")
106
- else
107
- file + "_spec.rb"
37
+
38
+ def spec_file?(file)
39
+ spec_files.include?(file)
108
40
  end
109
- end
110
-
111
- def spec_file?(file)
112
- file =~ /^spec\/.+_spec.rb$/
113
- end
114
- def cucumber_file?(file)
115
- file =~ /^features\/.+$/
41
+
42
+ def spec_files
43
+ @spec_files ||= Dir.glob("spec/**/*_spec.rb")
44
+ end
45
+
46
+ def append_spec_file_extension(file)
47
+ file.sub(/(\..*)$/, "_spec.rb")
48
+ end
49
+
116
50
  end
117
51
  end
118
52
  end
@@ -1,101 +1,33 @@
1
- require 'timeout'
2
-
3
1
  module RSpactor
4
- class Interactor
5
-
6
- attr_reader :runner
7
-
8
- def initialize(runner)
9
- @runner = runner
10
- ticker
11
- end
12
-
13
- def self.ticker_msg(msg, seconds_to_wait = 3, &block)
14
- $stdout.print msg
15
- if block
16
- @yielding_ticker = true
17
- Thread.new do
18
- loop do
19
- $stdout.print('.') if @yielding_ticker == true
20
- $stdout.flush
21
- sleep 1.0
22
- end
23
- end
24
-
25
- yield
26
- @yielding_ticker = false
27
- else
28
- seconds_to_wait.times do
29
- $stdout.print('.')
30
- $stdout.flush
31
- sleep 1
32
- end
33
- end
34
- $stdout.puts "\n"
35
- end
36
-
37
- def wait_for_enter_key(msg, seconds_to_wait, clear = runner.options[:clear])
38
- begin
39
- Timeout::timeout(seconds_to_wait) do
40
- system("clear;") if clear
41
- ticker(:start => true, :msg => msg)
42
- $stdin.gets
43
- return true
2
+ module Interactor
3
+ class << self
4
+
5
+ def init_signal_traps
6
+ # Ctrl-/
7
+ Signal.trap('QUIT') do
8
+ RSpactor.listener.stop
9
+ RSpactor.runner.start(:all => true)
10
+ RSpactor.listener.start
44
11
  end
45
- rescue Timeout::Error
46
- false
47
- ensure
48
- ticker(:stop => true)
49
- end
50
- end
51
-
52
- def start_termination_handler
53
- @main_thread = Thread.current
54
- Thread.new do
55
- loop do
56
- sleep 0.5
57
- if entry = $stdin.gets
58
- case entry
59
- when "c\n" # Cucumber: current tagged feature
60
- runner.run_cucumber_command
61
- when "ca\n" # Cucumber All: ~pending tagged feature
62
- runner.run_cucumber_command('~@wip,~@pending')
63
- when "r\n"
64
- runner.spork.reload if runner.spork
65
- else
66
- if wait_for_enter_key("** Running all specs... Hit <enter> again to exit RSpactor", 1)
67
- @main_thread.exit
68
- exit
69
- end
70
- runner.run_all_specs
71
- end
12
+ # Ctrl-C
13
+ Signal.trap('INT') do
14
+ if RSpactor.runner.run?
15
+ UI.info "RSpec run canceled", :reset => true, :clear => RSpactor.options[:clear]
16
+ RSpactor.runner.stop
17
+ else
18
+ UI.info "Bye bye...", :reset => true
19
+ abort("\n")
72
20
  end
73
21
  end
74
- end
75
- end
76
-
77
- private
78
-
79
- def ticker(opts = {})
80
- if opts[:stop]
81
- $stdout.puts "\n"
82
- @pointer_running = false
83
- elsif opts[:start]
84
- @pointer_running = true
85
- write(opts[:msg]) if opts[:msg]
86
- else
87
- Thread.new do
88
- loop do
89
- write('.') if @pointer_running == true
90
- sleep 1.0
91
- end
22
+ # Ctrl-Z
23
+ Signal.trap('TSTP') do
24
+ # UI.info "Reloading Spork...", :reset => true
25
+ RSpactor.listener.stop
26
+ # # Reload Spork
27
+ RSpactor.listener.start
92
28
  end
93
29
  end
94
- end
95
-
96
- def write(msg)
97
- $stdout.print(msg)
98
- $stdout.flush
30
+
99
31
  end
100
32
  end
101
33
  end
@@ -1,88 +1,59 @@
1
- require 'osx/foundation'
2
- OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
3
-
4
1
  module RSpactor
5
- # based on http://rails.aizatto.com/2007/11/28/taming-the-autotest-beast-with-fsevents/
6
2
  class Listener
7
- attr_reader :last_check, :callback, :valid_extensions
3
+ EXTENSIONS = %w[rb erb builder haml yml]
8
4
 
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
5
+ attr_reader :last_event, :callback, :pipe
6
+
7
+ def initialize
8
+ update_last_event
18
9
  end
19
10
 
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
11
+ def watch(&block)
12
+ @callback = block
41
13
  end
42
14
 
43
- def timestamp_checked
44
- @last_check = Time.now
15
+ def start
16
+ @pipe = IO.popen("#{bin_path}/fsevent_watch .")
17
+ watch_change
45
18
  end
46
19
 
47
- def split_paths(paths, num_events)
48
- paths.regard_as('*')
49
- rpaths = []
50
- num_events.times { |i| rpaths << paths[i] }
51
- rpaths
20
+ def stop
21
+ Process.kill("HUP", pipe.pid) if pipe
52
22
  end
53
23
 
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)
24
+ private
25
+
26
+ def watch_change
27
+ while !pipe.eof?
28
+ if line = pipe.readline
29
+ modified_dirs = line.split(" ")
30
+ files = modified_files(modified_dirs)
31
+ update_last_event
32
+ callback.call(files)
61
33
  end
62
34
  end
63
- changed_files
64
35
  end
65
36
 
66
- def file_changed?(file)
67
- File.stat(file).mtime > last_check
68
- rescue Errno::ENOENT
69
- false
37
+ def modified_files(dirs)
38
+ files = potentially_modified_files(dirs).select { |file| recent_file?(file) }
39
+ files.map! { |file| file.gsub("#{Dir.pwd}/", '') }
70
40
  end
71
41
 
72
- def ignore_path?(path)
73
- path =~ /(?:^|\/)\.(git|svn)/
42
+ def potentially_modified_files(dirs)
43
+ Dir.glob(dirs.map { |dir| "#{dir}*.{#{EXTENSIONS.join(',')}}" })
74
44
  end
75
45
 
76
- def ignore_file?(file)
77
- File.basename(file).index('.') == 0 or not valid_extension?(file)
46
+ def recent_file?(file)
47
+ File.mtime(file) >= last_event
78
48
  end
79
49
 
80
- def file_extension(file)
81
- file =~ /\.(\w+)$/ and $1
50
+ def update_last_event
51
+ @last_event = Time.now
82
52
  end
83
53
 
84
- def valid_extension?(file)
85
- valid_extensions.nil? or valid_extensions.include?(file_extension(file))
54
+ def bin_path
55
+ File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'bin'))
86
56
  end
57
+
87
58
  end
88
59
  end