rspactor 0.2.0 → 0.5.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 Andreas Wolff
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,58 @@
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 & cucumber features (much like autotest)."
13
+ gem.description = "read summary!"
14
+ gem.email = "guillaumegentil@gmail.com"
15
+ gem.homepage = "http://github.com/guillaumegentil/rspactor"
16
+ gem.authors = ["Mislav Marohnić", "Andreas Wolff", "Pelle Braendgaard", "Thibaud Guillaume-Gentil"]
17
+ gem.has_rdoc = false
18
+
19
+ gem.version = GEM_VERSION
20
+ gem.files = FileList['Rakefile', '{bin,lib,images,spec}/**/*', 'README*', 'LICENSE*']
21
+ gem.executables = Dir['bin/*'].map { |f| File.basename(f) }
22
+ end
23
+
24
+ spec_string = spec.to_ruby
25
+
26
+ begin
27
+ Thread.new { eval("$SAFE = 3\n#{spec_string}", binding) }.join
28
+ rescue
29
+ abort "unsafe gemspec: #{$!}"
30
+ else
31
+ File.open("#{spec.name}.gemspec", 'w') { |file| file.write spec_string }
32
+ end
33
+ end
34
+
35
+ task :bump => ["version:bump", :gemspec]
36
+
37
+ namespace :version do
38
+ task :read do
39
+ unless defined? GEM_VERSION
40
+ GEM_VERSION = File.read("VERSION")
41
+ end
42
+ end
43
+
44
+ task :bump => :read do
45
+ if ENV['VERSION']
46
+ GEM_VERSION.replace ENV['VERSION']
47
+ else
48
+ GEM_VERSION.sub!(/\d+$/) { |num| num.to_i + 1 }
49
+ end
50
+
51
+ File.open("VERSION", 'w') { |v| v.write GEM_VERSION }
52
+ end
53
+ end
54
+
55
+ task :release => :bump do
56
+ system %(git commit VERSION *.gemspec -m "release v#{GEM_VERSION}")
57
+ system %(git tag -am "release v#{GEM_VERSION}" v#{GEM_VERSION})
58
+ end
data/bin/rspactor CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib/rspactor'))
2
3
 
3
- require File.join(File.dirname(__FILE__), '..', 'lib', 'interactor')
4
- require File.join(File.dirname(__FILE__), '..', 'lib', 'listener')
5
- require File.join(File.dirname(__FILE__), '..', 'lib', 'inspector')
6
- require File.join(File.dirname(__FILE__), '..', 'lib', 'runner')
7
-
8
- Runner.load
4
+ RSpactor::Runner.start({
5
+ :coral => ARGV.delete('--coral'),
6
+ :celerity => ARGV.delete('--celerity'),
7
+ :spork => ARGV.delete('--drb'),
8
+ :view => ARGV.delete('--view'), # by default, rspactor didn't catch specs view
9
+ :clear => ARGV.delete('--clear'),
10
+ :run_in => ARGV.last
11
+ })
data/images/failed.png ADDED
Binary file
Binary file
Binary file
@@ -0,0 +1,61 @@
1
+ require 'cucumber'
2
+ require 'cucumber/formatter/console'
3
+ require File.dirname(__FILE__) + '/rspactor/growl'
4
+
5
+ module CucumberGrowler
6
+ include RSpactor::Growl
7
+
8
+ def self.included(base)
9
+ base.class_eval do
10
+ alias original_print_stats print_stats
11
+ include InstanceMethods
12
+
13
+ def print_stats(features)
14
+ title, icon, messages = '', '', []
15
+ [:failed, :skipped, :undefined, :pending, :passed].reverse.each do |status|
16
+ if step_mother.steps(status).any?
17
+ icon = icon_for(status)
18
+ # title = title_for(status)
19
+ messages << dump_count(step_mother.steps(status).length, "step", status.to_s)
20
+ end
21
+ end
22
+
23
+ notify "Cucumber Results", messages.reverse.join(", "), icon
24
+ original_print_stats(features)
25
+ end
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def icon_for(status)
31
+ case status
32
+ when :passed
33
+ 'success'
34
+ when :pending, :undefined, :skipped
35
+ 'pending'
36
+ when :failed
37
+ 'failed'
38
+ end
39
+ end
40
+
41
+ def title_for(status)
42
+ case status
43
+ when :passed
44
+ 'Features passed!'
45
+ when :pending
46
+ 'Some steps are pending...'
47
+ when :undefined
48
+ 'Some undefined steps...'
49
+ when :skipped
50
+ 'Some steps skipped...'
51
+ when :failed
52
+ 'Failures occurred!'
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ module Cucumber::Formatter::Console
60
+ include CucumberGrowler
61
+ end
@@ -0,0 +1,29 @@
1
+ require 'rspactor'
2
+
3
+ module RSpactor
4
+ class Celerity
5
+
6
+ def self.start(dir)
7
+ pid_path = "#{dir}/tmp/pids/mongrel_celerity.pid"
8
+ if File.exist?(pid_path)
9
+ system("kill $(head #{pid_path}) >/dev/null 2>&1")
10
+ system("rm #{pid_path} >/dev/null 2>&1")
11
+ end
12
+ # kill other mongrels
13
+ system("kill $(ps aux | grep 'mongrel_rails' | grep -v grep | awk '//{print $2;}') >/dev/null 2>&1")
14
+ system("rake celerity_server:start >/dev/null 2>&1 &")
15
+ Interactor.ticker_msg "** Starting celerity server"
16
+ end
17
+
18
+ def self.restart
19
+ system("rake celerity_server:stop >/dev/null 2>&1 && rake celerity_server:start >/dev/null 2>&1 &")
20
+ Interactor.ticker_msg "** Restarting celerity server"
21
+ end
22
+
23
+ def self.kill_jruby
24
+ system("kill $(ps aux | grep jruby | grep -v grep | awk '//{print $2;}') >/dev/null 2>&1")
25
+ true
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ module RSpactor
2
+ module Growl
3
+ extend self
4
+
5
+ def notify(title, msg, icon, pri = 0)
6
+ system("growlnotify -w -n rspactor --image #{image_path(icon)} -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,110 @@
1
+ require 'rspactor'
2
+
3
+ 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
+ candidates.reject { |candidate| candidate.index('.') }.each do |dir|
20
+ candidates.reject! { |candidate| candidate.index("#{dir}/") == 0 }
21
+ end
22
+ files = candidates.select { |candidate| File.exists? candidate }
23
+
24
+ if files.empty? && !candidates.empty? && !cucumberable
25
+ $stderr.puts "doesn't exist: #{candidates.inspect}"
26
+ end
27
+
28
+ files << 'cucumber' if cucumberable
29
+ files
30
+ end
31
+
32
+ # mappings for Rails are inspired by autotest mappings in rspec-rails
33
+ def translate(file)
34
+ file = file.sub(%r:^#{Regexp.escape(root)}/:, '')
35
+ candidates = []
36
+
37
+ if spec_file?(file)
38
+ candidates << file
39
+ elsif cucumber_file?(file)
40
+ candidates << 'cucumber'
41
+ else
42
+ spec_file = append_spec_file_extension(file)
43
+
44
+ case file
45
+ when %r:^app/:
46
+ if file =~ %r:^app/controllers/application(_controller)?.rb$:
47
+ candidates << 'controllers'
48
+ elsif file == 'app/helpers/application_helper.rb'
49
+ candidates << 'helpers' << 'views'
50
+ elsif !file.include?("app/views/") || runner.options[:view]
51
+ candidates << spec_file.sub('app/', '')
52
+
53
+ if file =~ %r:^app/(views/.+\.[a-z]+)\.[a-z]+$:
54
+ candidates << append_spec_file_extension($1)
55
+ elsif file =~ %r:app/helpers/(\w+)_helper.rb:
56
+ candidates << "views/#{$1}"
57
+ elsif file =~ /_observer.rb$/
58
+ candidates << candidates.last.sub('_observer', '')
59
+ end
60
+ end
61
+ when %r:^lib/:
62
+ candidates << spec_file
63
+ # lib/foo/bar_spec.rb -> lib/bar_spec.rb
64
+ candidates << candidates.last.sub($&, '')
65
+ # lib/bar_spec.rb -> bar_spec.rb
66
+ candidates << candidates.last.sub(%r:\w+/:, '') if candidates.last.index('/')
67
+ when 'config/routes.rb'
68
+ candidates << 'controllers' << 'helpers' << 'views' << 'routing'
69
+ when 'config/database.yml', 'db/schema.rb', 'spec/factories.rb'
70
+ candidates << 'models'
71
+ when 'config/boot.rb', 'config/environment.rb', %r:^config/environments/:, %r:^config/initializers/:, %r:^vendor/:, 'spec/spec_helper.rb'
72
+ Spork.reload if runner.options[:spork]
73
+ Celerity.restart if runner.options[:celerity]
74
+ candidates << 'spec'
75
+ when %r:^config/:
76
+ # nothing
77
+ when %r:^(spec/(spec_helper|shared/.*)|config/(boot|environment(s/test)?))\.rb$:, 'spec/spec.opts', 'spec/fakeweb.rb'
78
+ candidates << 'spec'
79
+ else
80
+ candidates << spec_file
81
+ end
82
+ end
83
+
84
+ candidates.map do |candidate|
85
+ if candidate == 'cucumber'
86
+ candidate
87
+ elsif candidate.index('spec') == 0
88
+ File.join(root, candidate)
89
+ else
90
+ File.join(root, 'spec', candidate)
91
+ end
92
+ end
93
+ end
94
+
95
+ def append_spec_file_extension(file)
96
+ if File.extname(file) == ".rb"
97
+ file.sub(/.rb$/, "_spec.rb")
98
+ else
99
+ file + "_spec.rb"
100
+ end
101
+ end
102
+
103
+ def spec_file?(file)
104
+ file =~ /^spec\/.+_spec.rb$/
105
+ end
106
+ def cucumber_file?(file)
107
+ file =~ /^features\/.+$/
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,85 @@
1
+ require 'timeout'
2
+
3
+ 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)
14
+ $stdout.print msg
15
+ seconds_to_wait.times do
16
+ $stdout.print('.')
17
+ $stdout.flush
18
+ sleep 1
19
+ end
20
+ $stdout.puts "\n"
21
+ end
22
+
23
+ def wait_for_enter_key(msg, seconds_to_wait, clear = runner.options[:clear])
24
+ begin
25
+ Timeout::timeout(seconds_to_wait) do
26
+ system("clear;") if clear
27
+ ticker(:start => true, :msg => msg)
28
+ $stdin.gets
29
+ return true
30
+ end
31
+ rescue Timeout::Error
32
+ false
33
+ ensure
34
+ ticker(:stop => true)
35
+ end
36
+ end
37
+
38
+ def start_termination_handler
39
+ @main_thread = Thread.current
40
+ Thread.new do
41
+ loop do
42
+ sleep 0.5
43
+ if entry = $stdin.gets
44
+ case entry
45
+ when "c\n" # Cucumber: current tagged feature
46
+ runner.run_cucumber_command
47
+ when "ca\n" # Cucumber All: ~pending tagged feature
48
+ runner.run_cucumber_command('~@wip,~@pending')
49
+ else
50
+ if wait_for_enter_key("** Running all specs... Hit <enter> again to exit RSpactor", 1)
51
+ @main_thread.exit
52
+ exit
53
+ end
54
+ runner.run_all_specs
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def ticker(opts = {})
64
+ if opts[:stop]
65
+ $stdout.puts "\n"
66
+ @pointer_running = false
67
+ elsif opts[:start]
68
+ @pointer_running = true
69
+ write(opts[:msg]) if opts[:msg]
70
+ else
71
+ Thread.new do
72
+ loop do
73
+ write('.') if @pointer_running == true
74
+ sleep 1.0
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def write(msg)
81
+ $stdout.print(msg)
82
+ $stdout.flush
83
+ end
84
+ end
85
+ 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,193 @@
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, now watching at '#{dir}'"
21
+ Spork.start if options[:spork]
22
+ Celerity.start(dir) if options[:celerity]
23
+ start_interactor
24
+ start_listener
25
+ end
26
+
27
+ def start_interactor
28
+ @interactor = Interactor.new(self)
29
+ aborted = @interactor.wait_for_enter_key("** Hit <enter> to skip initial spec & cucumber run", 2, false)
30
+ @interactor.start_termination_handler
31
+ unless aborted
32
+ run_all_specs
33
+ run_cucumber_command('~@wip,~@pending', false)
34
+ end
35
+ end
36
+
37
+ def start_listener
38
+ @inspector = Inspector.new(self)
39
+
40
+ Listener.new(Inspector::EXTENSIONS) do |files|
41
+ changed_files(files) unless git_head_changed?
42
+ end.run(dir)
43
+ end
44
+
45
+ def load_dotfile
46
+ dotfile = File.join(ENV['HOME'], '.rspactor')
47
+ if File.exists?(dotfile)
48
+ begin
49
+ Kernel.load dotfile
50
+ rescue => e
51
+ $stderr.puts "Error while loading #{dotfile}: #{e}"
52
+ end
53
+ end
54
+ end
55
+
56
+ def run_all_specs
57
+ run_spec_command(File.join(dir, 'spec'))
58
+ end
59
+
60
+ def run_spec_command(paths)
61
+ paths = Array(paths)
62
+ if paths.empty?
63
+ @last_run_failed = nil
64
+ else
65
+ cmd = [ruby_opts, spec_runner, paths, spec_opts].flatten.join(' ')
66
+ @last_run_failed = run_command(cmd)
67
+ end
68
+ end
69
+
70
+ def run_cucumber_command(tags = '@wip:2', clear = @options[:clear])
71
+ return unless File.exist?(File.join(dir, 'features'))
72
+
73
+ system("clear;") if clear
74
+ puts "** Running all #{tags} tagged features..."
75
+ cmd = [ruby_opts, cucumber_runner, cucumber_opts(tags)].flatten.join(' ')
76
+ @last_run_failed = run_command(cmd)
77
+ # Workaround for killing jruby process when used with celerity and spork
78
+ Celerity.kill_jruby if options[:celerity] && options[:spork]
79
+ end
80
+
81
+ def last_run_failed?
82
+ @last_run_failed == false
83
+ end
84
+
85
+ protected
86
+
87
+ def run_command(cmd)
88
+ system(cmd)
89
+ $?.success?
90
+ end
91
+
92
+ def changed_files(files)
93
+ files = files.inject([]) do |all, file|
94
+ all.concat inspector.determine_files(file)
95
+ end
96
+ unless files.empty?
97
+
98
+ # cucumber features
99
+ if files.delete('cucumber')
100
+ run_cucumber_command
101
+ end
102
+
103
+ # specs files
104
+ unless files.empty?
105
+ system("clear;") if @options[:clear]
106
+ files.uniq!
107
+ puts files.map { |f| f.to_s.gsub(/#{dir}/, '') }.join("\n")
108
+
109
+ previous_run_failed = last_run_failed?
110
+ run_spec_command(files)
111
+
112
+ if options[:retry_failed] and previous_run_failed and not last_run_failed?
113
+ run_all_specs
114
+ end
115
+ end
116
+ end
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 << spec_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 cucumber_opts(tags)
136
+ if File.exist?('features/support/cucumber.opts')
137
+ opts = File.read('features/support/cucumber.opts').gsub("\n", ' ')
138
+ else
139
+ opts = "--color --format progress --drb --no-profile"
140
+ end
141
+
142
+ opts << " --tags #{tags}"
143
+ opts << cucumber_formatter_opts
144
+ opts << " --require features" # because using require option overwrite default require
145
+ opts << " features"
146
+ opts
147
+ end
148
+
149
+ def spec_formatter_opts
150
+ " --require #{File.dirname(__FILE__)}/../rspec_growler.rb --format RSpecGrowler:STDOUT"
151
+ end
152
+
153
+ def cucumber_formatter_opts
154
+ " --require #{File.dirname(__FILE__)}/../cucumber_growler.rb"
155
+ end
156
+
157
+ def spec_runner
158
+ if File.exist?("script/spec")
159
+ "script/spec"
160
+ else
161
+ "spec"
162
+ end
163
+ end
164
+
165
+ def cucumber_runner
166
+ if File.exist?("script/cucumber")
167
+ "script/cucumber"
168
+ else
169
+ "cucumber"
170
+ end
171
+ end
172
+
173
+ def ruby_opts
174
+ other = ENV['RUBYOPT'] ? " #{ENV['RUBYOPT']}" : ''
175
+ other << ' -rcoral' if options[:coral]
176
+ %(RUBYOPT='-Ilib:spec#{other}')
177
+ end
178
+
179
+ def git_head_changed?
180
+ old_git_head = @git_head
181
+ read_git_head
182
+ @git_head and old_git_head and @git_head != old_git_head
183
+ end
184
+
185
+ def read_git_head
186
+ git_head_file = File.join(dir, '.git', 'HEAD')
187
+ @git_head = File.exists?(git_head_file) && File.read(git_head_file)
188
+ end
189
+ end
190
+ end
191
+
192
+ # backward compatibility
193
+ Runner = RSpactor::Runner