guard-rspec 3.1.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ require 'rspec/core'
2
+ require 'pathname'
3
+
4
+ module Guard
5
+ class RSpec
6
+ class Command < String
7
+ FAILURE_EXIT_CODE = 2
8
+
9
+ attr_accessor :paths, :options
10
+
11
+ def initialize(paths, options = {})
12
+ @paths = paths
13
+ @options = {
14
+ focus_on_failed: true,
15
+ notification: true,
16
+ cmd: 'rspec'
17
+ }.merge(options)
18
+
19
+ super(_parts.join(' '))
20
+ end
21
+
22
+ private
23
+
24
+ def _parts
25
+ parts = [options[:cmd]]
26
+ parts << _formatter
27
+ parts << _notifier
28
+ parts << _focuser
29
+ parts << "--failure-exit-code #{FAILURE_EXIT_CODE}"
30
+ parts << paths.join(' ')
31
+ end
32
+
33
+ def _formatter
34
+ return if _cmd_include_formatter?
35
+ _rspec_formatters || '-f progress'
36
+ end
37
+
38
+ def _rspec_formatters
39
+ formatters = ::RSpec::Core::ConfigurationOptions.new([]).parse_options()[:formatters] || nil
40
+ # RSpec's parser returns an array in the format [[formatter, output], ...], so match their format
41
+ # Construct a matching command line option, including output target
42
+ formatters && formatters.map { |formatter| "-f #{formatter.join ' -o '}" }.join(' ')
43
+ end
44
+
45
+ def _cmd_include_formatter?
46
+ options[:cmd] =~ /(?:^|\s)(?:-f\s*|--format(?:=|\s+))([\w:]+)/
47
+ end
48
+
49
+ def _notifier
50
+ return unless options[:notification]
51
+ "-r #{File.dirname(__FILE__)}/formatters/notifier.rb -f Guard::RSpec::Formatters::Notifier"
52
+ end
53
+
54
+ def _focuser
55
+ return unless options[:focus_on_failed]
56
+ "-r #{File.dirname(__FILE__)}/formatters/focuser.rb -f Guard::RSpec::Formatters::Focuser"
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,51 @@
1
+ module Guard
2
+ class RSpec
3
+ class Deprecator
4
+ attr_accessor :options
5
+
6
+ def self.warns_about_deprecated_options(options = {})
7
+ new(options).warns_about_deprecated_options
8
+ end
9
+
10
+ def initialize(options = {})
11
+ @options = options
12
+ end
13
+
14
+ def warns_about_deprecated_options
15
+ _spec_opts_env
16
+ _version_option
17
+ _exclude_option
18
+ _use_cmd_option
19
+ end
20
+
21
+ private
22
+
23
+ def _spec_opts_env
24
+ return if ENV['SPEC_OPTS'].nil?
25
+ UI.warning "The SPEC_OPTS environment variable is present. This can conflict with guard-rspec, particularly notifications."
26
+ end
27
+
28
+ def _version_option
29
+ return unless options.key?(:version)
30
+ _deprectated('The :version option is deprecated. Only RSpec ~> 2.14 is now supported.')
31
+ end
32
+
33
+ def _exclude_option
34
+ return unless options.key?(:exclude)
35
+ _deprectated('The :exclude option is deprecated. Please Guard ignore method instead. https://github.com/guard/guard#ignore')
36
+ end
37
+
38
+ def _use_cmd_option
39
+ %w[color drb fail_fast formatter env bundler binstubs rvm cli spring turnip zeus foreman].each do |option|
40
+ next unless options.key?(option.to_sym)
41
+ _deprectated("The :#{option} option is deprecated. Please customize the new :cmd option to fit your need.")
42
+ end
43
+ end
44
+
45
+ def _deprectated(message)
46
+ UI.warning %{Guard::RSpec DEPRECATION WARNING: #{message}}
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ require 'guard/rspec'
2
+ require 'rspec/core/formatters/base_formatter'
3
+
4
+ module Guard::RSpec::Formatters
5
+ class Focuser < ::RSpec::Core::Formatters::BaseFormatter
6
+
7
+ def dump_summary(duration, total, failures, pending)
8
+ _write_failed_paths_in_tmp if failures > 0
9
+ end
10
+
11
+ private
12
+
13
+ # Used for focus_on_failed options
14
+ def _write_failed_paths_in_tmp
15
+ FileUtils.mkdir_p('tmp')
16
+ File.open('./tmp/rspec_guard_result','w') do |f|
17
+ f.puts _failed_paths.join("\n")
18
+ end
19
+ rescue
20
+ # nothing really we can do, at least don't kill the test runner
21
+ end
22
+
23
+ def _failed_paths
24
+ failed = examples.select { |e| e.execution_result[:status] == 'failed' }
25
+ failed.map { |e| e.metadata[:location] }
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ require 'guard/rspec'
2
+ require 'guard/notifier'
3
+ require 'rspec/core/formatters/base_formatter'
4
+
5
+ module Guard::RSpec::Formatters
6
+ class Notifier < ::RSpec::Core::Formatters::BaseFormatter
7
+
8
+ def dump_summary(duration, total, failures, pending)
9
+ message = _message(total, failures, pending, duration)
10
+ status = _status(failures, pending)
11
+ _notify(message, status)
12
+ end
13
+
14
+ private
15
+
16
+ def _message(example_count, failure_count, pending_count, duration)
17
+ message = "#{example_count} examples, #{failure_count} failures"
18
+ if pending_count > 0
19
+ message << " (#{pending_count} pending)"
20
+ end
21
+ message << "\nin #{duration.round(4)} seconds"
22
+ message
23
+ end
24
+
25
+ def _status(failure_count, pending_count)
26
+ if failure_count > 0
27
+ :failed
28
+ elsif pending_count > 0
29
+ :pending
30
+ else
31
+ :success
32
+ end
33
+ end
34
+
35
+ def _notify(message, status)
36
+ Guard::Notifier.notify(message, title: 'RSpec results', image: status, priority: _priority(status))
37
+ end
38
+
39
+ def _priority(status)
40
+ { failed: 2,
41
+ pending: -1,
42
+ success: -2
43
+ }[status]
44
+ end
45
+
46
+ end
47
+ end
@@ -1,64 +1,68 @@
1
1
  module Guard
2
2
  class RSpec
3
3
  class Inspector
4
+ FOCUSED_FILE_PATH = './tmp/rspec_guard_result'
4
5
 
5
- attr_accessor :excluded, :spec_paths
6
+ attr_accessor :options, :failed_paths, :spec_paths
6
7
 
7
8
  def initialize(options = {})
8
- self.excluded = options.fetch(:exclude, [])
9
- self.spec_paths = options.fetch(:spec_paths, [])
10
- end
11
-
12
- def excluded=(pattern)
13
- @excluded = Dir[pattern.to_s]
14
- end
9
+ @options = {
10
+ focus_on_failed: true,
11
+ keep_failed: false,
12
+ spec_paths: %w[spec]
13
+ }.merge(options)
15
14
 
16
- def spec_paths=(paths)
17
- @spec_paths = Array(paths)
15
+ @failed_paths = []
16
+ @spec_paths = @options[:spec_paths]
18
17
  end
19
18
 
20
- def clean(paths)
21
- paths.uniq!
22
- paths.compact!
23
- clear_spec_files_list_after do
24
- paths = paths.select { |path| should_run_spec_file?(path) }
19
+ def paths(paths = nil)
20
+ if paths
21
+ _paths(paths)
22
+ else
23
+ spec_paths
25
24
  end
26
- paths.reject { |p| included_in_other_path?(p, paths) }
27
- end
28
-
29
- private
30
-
31
- def should_run_spec_file?(path)
32
- (spec_file?(path) || feature_file?(path) || spec_folder?(path)) && !excluded.include?(path)
33
25
  end
34
26
 
35
- def spec_file?(path)
36
- spec_files.include?(path)
37
- end
38
-
39
- def feature_file?(path)
40
- feature_files.include?(path)
27
+ def clear_paths(paths = nil)
28
+ if paths
29
+ @failed_paths -= paths
30
+ else
31
+ @failed_paths.clear
32
+ end
41
33
  end
42
34
 
43
- def spec_folder?(path)
44
- path.match(%r{^(#{spec_paths.join("|")})[^\.]*$})
45
- end
35
+ private
46
36
 
47
- def spec_files
48
- @spec_files ||= spec_paths.collect { |path| Dir[File.join(path, "**{,/*/**}", "*[_.]spec.rb")] }.flatten
37
+ def _paths(paths)
38
+ _focused_paths || if options[:keep_failed]
39
+ @failed_paths += _clean(paths)
40
+ else
41
+ _clean(paths)
42
+ end
49
43
  end
50
44
 
51
- def feature_files
52
- @feature_files ||= spec_paths.collect { |path| Dir[File.join(path, "**{,/*/**}", "*.feature")] }.flatten
45
+ def _focused_paths
46
+ return nil unless options[:focus_on_failed]
47
+ File.open(FOCUSED_FILE_PATH).read.split("\n")[0..10]
48
+ rescue
49
+ nil
50
+ ensure
51
+ File.exist?(FOCUSED_FILE_PATH) && File.delete(FOCUSED_FILE_PATH)
53
52
  end
54
53
 
55
- def clear_spec_files_list_after
56
- yield
57
- @spec_files = nil
54
+ def _clean(paths)
55
+ paths.uniq!
56
+ paths.compact!
57
+ paths = _select_only_spec_files(paths)
58
+ paths
58
59
  end
59
60
 
60
- def included_in_other_path?(path, paths)
61
- (paths - [path]).any? { |p| path.include?(p) && path.sub(p, '').include?('/') }
61
+ def _select_only_spec_files(paths)
62
+ spec_files = spec_paths.collect { |path| Dir[File.join(path, "**{,/*/**}", "*[_.]spec.rb")] }
63
+ feature_files = spec_paths.collect { |path| Dir[File.join(path, "**{,/*/**}", "*.feature")] }
64
+ files = (spec_files + feature_files).flatten
65
+ paths.select { |p| files.include?(p) }
62
66
  end
63
67
 
64
68
  end
@@ -1,285 +1,84 @@
1
- require 'drb/drb'
2
- require 'rspec'
3
- require 'pathname'
1
+ require 'guard/rspec/command'
2
+ require 'guard/rspec/inspector'
4
3
 
5
4
  module Guard
6
5
  class RSpec
7
6
  class Runner
7
+ attr_accessor :options, :inspector
8
8
 
9
- FAILURE_EXIT_CODE = 2
10
-
11
- attr_accessor :options
12
9
  def initialize(options = {})
13
10
  @options = {
14
- :bundler => true,
15
- :binstubs => false,
16
- :rvm => nil,
17
- :cli => nil,
18
- :env => nil,
19
- :launchy => nil,
20
- :notification => true,
21
- :spring => false,
22
- :turnip => false,
23
- :zeus => false,
24
- :foreman => false
11
+ all_after_pass: false,
12
+ notification: true,
13
+ run_all: { message: 'Running all specs' },
14
+ launchy: nil
25
15
  }.merge(options)
26
16
 
27
- unless ENV['SPEC_OPTS'].nil?
28
- UI.warning "The SPEC_OPTS environment variable is present. This can conflict with guard-rspec, particularly notifications."
29
- end
30
-
31
- if options[:bundler] && !options[:binstubs]
32
- if options[:zeus]
33
- UI.warning "Running Zeus within bundler is waste of time. Bundler option is set to false, when using Zeus."
34
- elsif options[:spring]
35
- UI.warning "Running Spring within bundler is waste of time. Bundler option is set to false, when using Spring."
36
- end
37
- end
38
-
39
- deprecations_warnings
40
- end
41
-
42
- def run(paths, options = {})
43
- return false if paths.empty?
44
-
45
- message = options[:message] || "Running: #{paths.join(' ')}"
46
- UI.info(message, :reset => true)
47
-
48
- options = @options.merge(options)
49
-
50
- if drb_used?
51
- run_via_drb(paths, options)
52
- else
53
- run_via_shell(paths, options)
54
- end
55
- end
56
-
57
- def rspec_executable(runtime_options = {})
58
- command = parallel?(runtime_options) ? 'parallel_rspec' : 'rspec'
59
- @rspec_executable ||= (binstubs? && !executable_prefix?) ? "#{binstubs}/#{command}" : command
60
- end
61
-
62
- def failure_exit_code_supported?
63
- @failure_exit_code_supported ||= begin
64
- cmd_parts = []
65
- cmd_parts << "bundle exec" if bundle_exec?
66
- cmd_parts << rspec_executable
67
- cmd_parts << "--help"
68
- `#{cmd_parts.join(' ')}`.include? "--failure-exit-code"
69
- end
70
- end
71
-
72
- def parsed_or_default_formatter
73
- @parsed_or_default_formatter ||= begin
74
- # Use RSpec's parser to parse formatters
75
- formatters = ::RSpec::Core::ConfigurationOptions.new([]).parse_options()[:formatters]
76
- # Use a default formatter if none exists.
77
- # RSpec's parser returns an array in the format [[formatter, output], ...], so match their format
78
- formatters = [['progress']] if formatters.nil? || formatters.empty?
79
- # Construct a matching command line option, including output target
80
- formatters.map { |formatter| "-f #{formatter.join ' -o '}" }.join ' '
81
- end
82
- end
83
-
84
- private
85
-
86
- def environment_variables
87
- return if options[:env].nil?
88
- "export " + options[:env].map {|key, value| "#{key}=#{value}"}.join(' ') + ';'
89
- end
90
-
91
- def rspec_arguments(paths, options)
92
- arg_parts = []
93
- arg_parts << options[:cli]
94
- if options[:notification]
95
- arg_parts << parsed_or_default_formatter unless options[:cli] =~ formatter_regex
96
- arg_parts << "-r #{zeus_guard_env_file.path}" if zeus?
97
- arg_parts << "-r #{File.dirname(__FILE__)}/formatter.rb"
98
- arg_parts << "-f Guard::RSpec::Formatter"
99
- end
100
- arg_parts << "--failure-exit-code #{FAILURE_EXIT_CODE}" if failure_exit_code_supported?
101
- arg_parts << "-r turnip/rspec" if options[:turnip]
102
- arg_parts << paths.join(' ')
103
-
104
- arg_parts.compact.join(' ')
105
- end
106
-
107
- def zeus_guard_env_file
108
- unless @zeus_guard_env_file
109
- @zeus_guard_env_file = Tempfile.new(['zeus_guard_env','.rb'])
110
- @zeus_guard_env_file.puts '# encoding: UTF-8'
111
- @zeus_guard_env_file.puts '# Extra settings for Guard when using Zeus'
112
- @zeus_guard_env_file.puts "ENV['GUARD_NOTIFICATIONS']=#{ENV['GUARD_NOTIFICATIONS'].inspect}" if ENV['GUARD_NOTIFICATIONS']
113
- @zeus_guard_env_file.puts "ENV['GUARD_NOTIFY']=#{ENV['GUARD_NOTIFY'].inspect}" if ENV['GUARD_NOTIFY']
114
- @zeus_guard_env_file.close
115
- end
116
-
117
- @zeus_guard_env_file
118
- end
119
-
120
- def parallel_rspec_arguments(paths, options)
121
- arg_parts = []
122
- arg_parts << options[:parallel_cli]
123
- arg_parts << "-o '#{rspec_arguments([], options).strip}'"
124
- arg_parts << paths.join(' ')
125
-
126
- arg_parts.compact.join(' ')
17
+ @inspector = Inspector.new(@options)
127
18
  end
128
19
 
129
- def rspec_command(paths, options)
130
- cmd_parts = []
131
- cmd_parts << environment_variables
132
- cmd_parts << "rvm #{options[:rvm].join(',')} exec" if options[:rvm].respond_to?(:join)
133
- cmd_parts << bin_command('foreman run') if foreman?
134
- cmd_parts << "bundle exec" if bundle_exec?
135
- cmd_parts << executable_prefix if executable_prefix?
136
- cmd_parts << rspec_executable(options)
137
- cmd_parts << rspec_arguments(paths, options) if !parallel?(options)
138
- cmd_parts << parallel_rspec_arguments(paths, options) if parallel?(options)
139
- cmd_parts.compact.join(' ')
140
- end
20
+ def run_all
21
+ options = @options.merge(@options[:run_all])
22
+ UI.info(options[:message], reset: true)
141
23
 
142
- def run_via_shell(paths, options)
143
- success = system(rspec_command(paths, options))
144
-
145
- if options[:notification] && !drb_used? && !success && rspec_command_exited_with_an_exception?
146
- Notifier.notify("Failed", :title => "RSpec results", :image => :failed, :priority => 2)
147
- end
148
-
149
- if options[:launchy]
150
- require 'launchy'
151
- pn = Pathname.new(options[:launchy])
152
- if pn.exist?
153
- Launchy.open(options[:launchy])
154
- end
155
- end
156
- success
24
+ _run(inspector.paths, [], options)
157
25
  end
158
26
 
159
- def rspec_command_exited_with_an_exception?
160
- failure_exit_code_supported? && $?.exitstatus != FAILURE_EXIT_CODE
161
- end
27
+ def run(paths)
28
+ failed_paths = inspector.failed_paths
29
+ paths = inspector.paths(paths)
30
+ return if paths.empty?
162
31
 
163
- # We can optimize this path by hitting up the drb server directly, circumventing the overhead
164
- # of the user's shell, bundler and ruby environment.
165
- def run_via_drb(paths, options)
166
- require "shellwords"
167
- argv = rspec_arguments(paths, options).shellsplit
32
+ UI.info("Running: #{paths.join(' ')}", reset: true)
168
33
 
169
- # The user can specify --drb-port for rspec, we need to honor it.
170
- if idx = argv.index("--drb-port")
171
- port = argv[idx + 1].to_i
172
- end
173
- port = ENV["RSPEC_DRB"] || 8989 unless port && port > 0
174
- ret = drb_service(port.to_i).run(argv, $stderr, $stdout)
175
-
176
- [0, true].include?(ret)
177
- rescue DRb::DRbConnError
178
- # Fall back to the shell runner; we don't want to mangle the environment!
179
- run_via_shell(paths, options)
180
- end
181
-
182
- def drb_used?
183
- @drb_used ||= options[:cli] && options[:cli].include?('--drb')
34
+ _run(paths, failed_paths, options)
184
35
  end
185
36
 
186
- # W we can avoid loading a large chunk of rspec
187
- # just to let DRb know what to do.
188
- #
189
- # For reference:
190
- #
191
- # * RSpec: https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/drb_command_line.rb
192
- def drb_service(port)
193
- require "drb/drb"
194
-
195
- # Make sure we have a listener running
196
- unless @drb_listener_running
197
- begin
198
- DRb.start_service("druby://127.0.0.1:0")
199
- rescue SocketError, Errno::EADDRNOTAVAIL
200
- DRb.start_service("druby://:0")
201
- end
202
-
203
- @drb_listener_running = true
204
- end
205
-
206
- @drb_services ||= {}
207
- @drb_services[port.to_i] ||= DRbObject.new_with_uri("druby://127.0.0.1:#{port}")
37
+ def reload
38
+ inspector.clear_paths
208
39
  end
209
40
 
210
- def bundler_allowed?
211
- @bundler_allowed ||= (File.exist?("#{Dir.pwd}/Gemfile") && !zeus? && !spring?)
212
- end
213
-
214
- def bundler?
215
- @bundler ||= bundler_allowed? && @options[:bundler]
216
- end
217
-
218
- def binstubs?
219
- @binstubs ||= !!@options[:binstubs]
220
- end
221
-
222
- def executable_prefix?
223
- zeus? || spring? || foreman?
224
- end
41
+ private
225
42
 
226
- def executable_prefix
227
- if zeus?
228
- bin_command('zeus')
229
- elsif spring? && !parallel?
230
- bin_command('spring')
43
+ def _run(paths, failed_paths, options)
44
+ command = Command.new(paths, options)
45
+ _without_bundler_env { Kernel.system(command) }.tap do |success|
46
+ success ? inspector.clear_paths(paths) : _notify_failure
47
+ _open_launchy
48
+ _run_all_after_pass(success, failed_paths)
231
49
  end
232
50
  end
233
51
 
234
- def zeus?
235
- options.fetch(:zeus, false)
236
- end
237
-
238
- def parallel?(runtime_options = {})
239
- if runtime_options[:run_all_specs]
240
- runtime_options[:parallel]
52
+ def _without_bundler_env
53
+ if defined?(Bundler)
54
+ Bundler.with_clean_env { yield }
241
55
  else
242
- options.fetch(:parallel, false)
56
+ yield
243
57
  end
244
58
  end
245
59
 
246
- def spring?
247
- options.fetch(:spring, false)
248
- end
249
-
250
- def foreman?
251
- options.fetch(:foreman, false)
252
- end
253
-
254
- def binstubs
255
- options[:binstubs] == true ? "bin" : options[:binstubs]
60
+ def _notify_failure
61
+ return unless options[:notification]
62
+ return unless command_exception?
63
+ Notifier.notify('Failed', title: 'RSpec results', image: :failed, priority: 2)
256
64
  end
257
65
 
258
- def bin_command(command)
259
- binstubs? ? "#{binstubs}/#{command}" : command
66
+ def command_exception?
67
+ $?.exitstatus != Command::FAILURE_EXIT_CODE
260
68
  end
261
69
 
262
- def bundle_exec?
263
- bundler? && !binstubs?
70
+ def _open_launchy
71
+ return unless options[:launchy]
72
+ require 'launchy'
73
+ pn = Pathname.new(options[:launchy])
74
+ Launchy.open(options[:launchy]) if pn.exist?
264
75
  end
265
76
 
266
- def deprecations_warnings
267
- [:color, :drb, [:fail_fast, "fail-fast"], [:formatter, "format"]].each do |option|
268
- key, value = option.is_a?(Array) ? option : [option, option.to_s]
269
- if options.key?(key)
270
- @options.delete(key)
271
- UI.info %{DEPRECATION WARNING: The :#{key} option is deprecated. Pass standard command line argument "--#{value}" to RSpec with the :cli option.}
272
- end
273
- end
274
- if options.key?(:version)
275
- @options.delete(:version)
276
- UI.info %{DEPRECATION WARNING: The :version option is deprecated. Only RSpec 2 is now supported.}
277
- end
77
+ def _run_all_after_pass(success, failed_paths)
78
+ return unless options[:all_after_pass]
79
+ run_all if success && !failed_paths.empty?
278
80
  end
279
81
 
280
- def formatter_regex
281
- @formatter_regex ||= /(?:^|\s)(?:-f\s*|--format(?:=|\s+))([\w:]+)/
282
- end
283
82
  end
284
83
  end
285
84
  end