rspactor 0.2.0 → 0.5.4

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/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