guard-rspec 3.1.0 → 4.0.0

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.
@@ -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