guard 2.7.0 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,6 +2,7 @@ require "guard/ui"
2
2
 
3
3
  require "guard/jobs/sleep"
4
4
  require "guard/jobs/pry_wrapper"
5
+ require "guard/jobs/stdin"
5
6
 
6
7
  module Guard
7
8
  class Interactor
@@ -12,7 +13,11 @@ module Guard
12
13
  def initialize(no_interaction = false)
13
14
  @interactive = !no_interaction && self.class.enabled?
14
15
 
15
- job_klass = interactive? ? Jobs::PryWrapper : Jobs::Sleep
16
+
17
+ # TODO: restore
18
+ # job_klass = interactive? ? Jobs::PryWrapper : Jobs::Sleep
19
+ job_klass = Jobs::StdinJob
20
+
16
21
  @idle_job = job_klass.new(self.class.options)
17
22
  end
18
23
 
@@ -74,6 +74,7 @@ module Guard
74
74
  _start_pry
75
75
  @thread.join
76
76
  thread = @thread
77
+ # TODO: rename :stopped to continue
77
78
  thread.nil? ? :stopped : :exit
78
79
  ensure
79
80
  ::Guard::UI.reset_line
@@ -0,0 +1,46 @@
1
+ require "guard/jobs/base"
2
+
3
+ module Guard
4
+ module Jobs
5
+ class StdinJob
6
+ def initialize(options)
7
+ @mode = :stopped
8
+ @sleeping = false
9
+ end
10
+
11
+ def foreground
12
+ output "Guard is idle" # needed for child-process cucumber tests
13
+
14
+ line = $stdin.readline.chomp
15
+ return :exit if line == "exit"
16
+
17
+ m = /^sleep (?<seconds>\d+)$/.match(line)
18
+ return @mode unless m
19
+
20
+ seconds = Integer(m[:seconds][/\d+/])
21
+ @sleeping = true
22
+ sleep seconds
23
+ @sleeping = false
24
+ @mode
25
+ rescue EOFError, Interrupt
26
+ @sleeping = false
27
+ :exit
28
+ end
29
+
30
+ def background
31
+ Thread.main.wakeup if @sleeping
32
+ end
33
+
34
+ def handle_interrupt
35
+ @mode = :exit
36
+ end
37
+
38
+ private
39
+
40
+ def output(text)
41
+ $stdout.puts text
42
+ $stdout.flush
43
+ end
44
+ end
45
+ end
46
+ end
@@ -18,18 +18,13 @@ module Guard
18
18
  }
19
19
 
20
20
  def self.available?(opts = {})
21
- super && _emacs_client_available?(opts)
22
- end
21
+ return false unless super
23
22
 
24
- # @private
25
- #
26
- # @return [Boolean] whether or not the emacs client is available
27
- #
28
- def self._emacs_client_available?(opts)
29
23
  client_name = opts.fetch(:client, DEFAULTS[:client])
30
24
  cmd = "#{client_name} --eval '1' 2> #{DEV_NULL} || echo 'N/A'"
31
25
  stdout = Sheller.stdout(cmd)
32
- !%w(N/A 'N/A').include?(stdout.chomp!)
26
+ return false if stdout.nil?
27
+ !%w(N/A 'N/A').include?(stdout.chomp)
33
28
  end
34
29
 
35
30
  # Shows a system notification.
@@ -0,0 +1,103 @@
1
+ require "guard/notifiers/base"
2
+
3
+ module Guard
4
+ module Notifier
5
+ # Send a notification to Emacs with emacsclient
6
+ # (http://www.emacswiki.org/emacs/EmacsClient).
7
+ #
8
+ # @example Add the `:emacs` notifier to your `Guardfile`
9
+ # notification :emacs
10
+ #
11
+ class Emacs < Base
12
+ DEFAULTS = {
13
+ client: "emacsclient",
14
+ success: "ForestGreen",
15
+ failed: "Firebrick",
16
+ default: "Black",
17
+ fontcolor: "White",
18
+ }
19
+
20
+ def self.available?(opts = {})
21
+ super && _emacs_client_available?(opts)
22
+ end
23
+
24
+ # @private
25
+ #
26
+ # @return [Boolean] whether or not the emacs client is available
27
+ #
28
+ def self._emacs_client_available?(opts)
29
+ client_name = opts.fetch(:client, DEFAULTS[:client])
30
+ cmd = "#{client_name} --eval '1' 2> #{DEV_NULL} || echo 'N/A'"
31
+ stdout = Sheller.stdout(cmd)
32
+ !%w(N/A 'N/A').include?(stdout.chomp!)
33
+ end
34
+
35
+ # Shows a system notification.
36
+ #
37
+ # @param [String] type the notification type. Either 'success',
38
+ # 'pending', 'failed' or 'notify'
39
+ # @param [String] title the notification title
40
+ # @param [String] message the notification message body
41
+ # @param [String] image the path to the notification image
42
+ # @param [Hash] opts additional notification library options
43
+ # @option opts [String] success the color to use for success
44
+ # notifications (default is 'ForestGreen')
45
+ # @option opts [String] failed the color to use for failure
46
+ # notifications (default is 'Firebrick')
47
+ # @option opts [String] pending the color to use for pending
48
+ # notifications
49
+ # @option opts [String] default the default color to use (default is
50
+ # 'Black')
51
+ # @option opts [String] client the client to use for notification
52
+ # (default is 'emacsclient')
53
+ # @option opts [String, Integer] priority specify an int or named key
54
+ # (default is 0)
55
+ #
56
+ def notify(message, opts = {})
57
+ super
58
+
59
+ opts = DEFAULTS.merge(opts)
60
+ color = emacs_color(opts[:type], opts)
61
+ fontcolor = emacs_color(:fontcolor, opts)
62
+ elisp = <<-EOF.gsub(/\s+/, " ").strip
63
+ (set-face-attribute 'mode-line nil
64
+ :background "#{color}"
65
+ :foreground "#{fontcolor}")
66
+ EOF
67
+
68
+ _run_cmd(opts[:client], "--eval", elisp)
69
+ end
70
+
71
+ # Get the Emacs color for the notification type.
72
+ # You can configure your own color by overwrite the defaults.
73
+ #
74
+ # @param [String] type the notification type
75
+ # @param [Hash] options aditional notification options
76
+ #
77
+ # @option options [String] success the color to use for success
78
+ # notifications (default is 'ForestGreen')
79
+ #
80
+ # @option options [String] failed the color to use for failure
81
+ # notifications (default is 'Firebrick')
82
+ #
83
+ # @option options [String] pending the color to use for pending
84
+ # notifications
85
+ #
86
+ # @option options [String] default the default color to use (default is
87
+ # 'Black')
88
+ #
89
+ # @return [String] the name of the emacs color
90
+ #
91
+ def emacs_color(type, options = {})
92
+ default = options.fetch(:default, DEFAULTS[:default])
93
+ options.fetch(type.to_sym, default)
94
+ end
95
+
96
+ private
97
+
98
+ def _run_cmd(cmd, *args)
99
+ Sheller.run(cmd, *args)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -69,10 +69,12 @@ module Guard
69
69
  # https://github.com/guard/guard/wiki/Upgrading-to-Guard-2.0
70
70
  #
71
71
  def initialize_plugin(options)
72
- if plugin_class.superclass.to_s == "Guard::Guard"
73
- plugin_class.new(options.delete(:watchers), options)
72
+ klass = plugin_class
73
+ fail "Could not load class: #{_constant_name.inspect}" unless klass
74
+ if klass.superclass.to_s == "Guard::Guard"
75
+ klass.new(options.delete(:watchers), options)
74
76
  else
75
- plugin_class.new(options)
77
+ klass.new(options)
76
78
  end
77
79
  end
78
80
 
data/lib/guard/runner.rb CHANGED
@@ -14,12 +14,10 @@ module Guard
14
14
  # @param [Hash] scopes either the Guard plugin or the group to run the task
15
15
  # on
16
16
  #
17
- # @see self.run_supervised_task
18
- #
19
- def run(task, scope = {})
17
+ def run(task, scope_hash = {})
20
18
  Lumberjack.unit_of_work do
21
- _scoped_plugins(scope) do |guard|
22
- run_supervised_task(guard, task) if guard.respond_to?(task)
19
+ _scoped_plugins(scope_hash || {}) do |plugin|
20
+ _supervise(plugin, task) if plugin.respond_to?(task)
23
21
  end
24
22
  end
25
23
  end
@@ -47,15 +45,16 @@ module Guard
47
45
 
48
46
  ::Guard::UI.clearable
49
47
 
50
- _scoped_plugins do |guard|
48
+ _scoped_plugins do |plugin|
51
49
  ::Guard::UI.clear
52
50
 
53
51
  types.each do |tasks, unmatched_paths|
54
- paths = ::Guard::Watcher.match_files(guard, unmatched_paths)
55
- next if paths.empty?
52
+ next if unmatched_paths.empty?
53
+ match_result = ::Guard::Watcher.match_files(plugin, unmatched_paths)
54
+ next if match_result.empty?
56
55
 
57
- next unless (task = tasks.detect { |meth| guard.respond_to?(meth) })
58
- run_supervised_task(guard, task, paths)
56
+ next unless (task = tasks.detect { |meth| plugin.respond_to?(meth) })
57
+ _supervise(plugin, task, match_result)
59
58
  end
60
59
  end
61
60
  end
@@ -71,24 +70,24 @@ module Guard
71
70
  # @param [Array] args the arguments for the task
72
71
  # @raise [:task_has_failed] when task has failed
73
72
  #
74
- def run_supervised_task(guard, task, *args)
75
- catch self.class.stopping_symbol_for(guard) do
76
- guard.hook("#{ task }_begin", *args)
73
+ def _supervise(plugin, task, *args)
74
+ catch self.class.stopping_symbol_for(plugin) do
75
+ plugin.hook("#{ task }_begin", *args)
77
76
  begin
78
- result = guard.send(task, *args)
77
+ result = plugin.send(task, *args)
79
78
  rescue Interrupt
80
79
  throw(:task_has_failed)
81
80
  end
82
- guard.hook("#{ task }_end", result)
81
+ plugin.hook("#{ task }_end", result)
83
82
  result
84
83
  end
85
84
  rescue ScriptError, StandardError, RuntimeError
86
- ::Guard::UI.error("#{ guard.class.name } failed to achieve its"\
85
+ ::Guard::UI.error("#{ plugin.class.name } failed to achieve its"\
87
86
  " <#{ task }>, exception was:" \
88
87
  "\n#{ $!.class }: #{ $!.message }" \
89
88
  "\n#{ $!.backtrace.join("\n") }")
90
- ::Guard.plugins.delete guard
91
- ::Guard::UI.info("\n#{ guard.class.name } has just been fired")
89
+ ::Guard.remove_plugin(plugin)
90
+ ::Guard::UI.info("\n#{ plugin.class.name } has just been fired")
92
91
  $!
93
92
  end
94
93
 
@@ -121,12 +120,14 @@ module Guard
121
120
  # @yield the task to run
122
121
  #
123
122
  def _scoped_plugins(scopes = {})
123
+ fail "NO PLUGIN SCOPE" if scopes.nil?
124
124
  if plugins = _current_plugins_scope(scopes)
125
125
  plugins.each do |guard|
126
126
  yield(guard)
127
127
  end
128
128
  else
129
129
  _current_groups_scope(scopes).each do |group|
130
+ fail "Invalid group: #{group.inspect}" unless group.respond_to?(:name)
130
131
  current_plugin = nil
131
132
  block_return = catch :task_has_failed do
132
133
  ::Guard.plugins(group: group.name).each do |guard|
@@ -151,13 +152,9 @@ module Guard
151
152
  # @return [Array<Guard::Plugin>] the plugins to scope to
152
153
  #
153
154
  def _current_plugins_scope(scope)
154
- if plugins = _find_non_empty_plugins_scope(scope)
155
- Array(plugins).map do |plugin|
156
- plugin.is_a?(Symbol) ? ::Guard.plugin(plugin) : plugin
157
- end
158
- else
159
- nil
160
- end
155
+ return nil unless (plugins = _find_non_empty_plugins_scope(scope))
156
+
157
+ Array(plugins).map { |plugin| _instantiate(:plugin, plugin) }
161
158
  end
162
159
 
163
160
  # Returns the current groups scope.
@@ -168,9 +165,14 @@ module Guard
168
165
  # @return [Array<Guard::Group>] the groups to scope to
169
166
  #
170
167
  def _current_groups_scope(scope)
171
- Array(_find_non_empty_groups_scope(scope)).map do |group|
172
- group.is_a?(Symbol) ? ::Guard.group(group) : group
173
- end
168
+ found = _find_non_empty_groups_scope(scope)
169
+ groups = Array(found).map { |group| _instantiate(:group, group) }
170
+ return groups if groups.any? { |g| g.name == :common }
171
+ ([_instantiate(:group, :common)] + Array(found)).compact
172
+ end
173
+
174
+ def _instantiate(meth, obj)
175
+ (obj.is_a?(Symbol) || obj.is_a?(String)) ? ::Guard.send(meth, obj) : obj
174
176
  end
175
177
 
176
178
  # Find the first non empty element in the given possibilities
@@ -178,16 +180,17 @@ module Guard
178
180
  def _find_non_empty_scope(type, local_scope, *additional_possibilities)
179
181
  found = [
180
182
  local_scope[:"#{type}s"],
181
- local_scope[type.to_sym],
183
+ [local_scope[type.to_sym]],
182
184
  ::Guard.scope[:"#{type}s"],
183
185
  additional_possibilities.flatten
184
- ].compact.detect { |a| !Array(a).empty? }
185
- found ? [::Guard.group(:common)] + Array(found) : found
186
+ ]
187
+ found.compact.detect { |a| !Array(a).compact.empty? }
186
188
  end
187
189
 
188
190
  # Find the first non empty plugins scope
189
191
  #
190
192
  def _find_non_empty_plugins_scope(scope)
193
+ fail "NO PLUGIN SCOPE" if scope.nil?
191
194
  _find_non_empty_scope(:plugin, scope)
192
195
  end
193
196
 
data/lib/guard/setuper.rb CHANGED
@@ -51,7 +51,7 @@ module Guard
51
51
  @runner = ::Guard::Runner.new
52
52
  @watchdirs = _setup_watchdirs
53
53
 
54
- ::Guard::UI.clear(force: true)
54
+ ::Guard::UI.setup(options)
55
55
 
56
56
  _setup_debug if options[:debug]
57
57
  @listener = _setup_listener
@@ -62,7 +62,7 @@ module Guard
62
62
  self
63
63
  end
64
64
 
65
- attr_reader :options, :evaluator, :interactor
65
+ attr_reader :options, :interactor
66
66
 
67
67
  # Used only by tests (for all I know...)
68
68
  def clear_options
@@ -127,12 +127,11 @@ module Guard
127
127
  def setup_scope(scope = {})
128
128
  # TODO: there should be a special Scope class instead
129
129
  scope = _prepare_scope(scope)
130
- { groups: :add_group, plugins: :plugin }.each do |type, meth|
131
- next unless scope[type].any?
132
- ::Guard.scope[type] = scope[type].map do |item|
133
- ::Guard.send(meth, item)
134
- end
135
- end
130
+
131
+ ::Guard.scope = {
132
+ groups: scope[:groups].map { |item| ::Guard.add_group(item) },
133
+ plugins: scope[:plugins].map { |item| ::Guard.plugin(item) },
134
+ }
136
135
  end
137
136
 
138
137
  # Evaluates the Guardfile content. It displays an error message if no
@@ -143,7 +142,7 @@ module Guard
143
142
  def evaluate_guardfile
144
143
  evaluator.evaluate_guardfile
145
144
  msg = "No plugins found in Guardfile, please add at least one."
146
- ::Guard::UI.error msg unless _non_builtin_plugins?
145
+ ::Guard::UI.error msg unless _pluginless_guardfile?
147
146
  end
148
147
 
149
148
  # Asynchronously trigger changes
@@ -343,16 +342,30 @@ module Guard
343
342
  end
344
343
 
345
344
  def _prepare_scope(scope)
345
+ fail "Guard::setup() not called!" if options.nil?
346
346
  plugins = Array(options[:plugin])
347
347
  plugins = Array(scope[:plugins] || scope[:plugin]) if plugins.empty?
348
348
 
349
+ # Convert objects to names
350
+ plugins.map! { |p| p.respond_to?(:name) ? p.name : p }
351
+
349
352
  groups = Array(options[:group])
350
353
  groups = Array(scope[:groups] || scope[:group]) if groups.empty?
351
354
 
355
+ # Convert objects to names
356
+ groups.map! { |g| g.respond_to?(:name) ? g.name : g }
357
+
352
358
  { plugins: plugins, groups: groups }
353
359
  end
354
360
 
355
- def _non_builtin_plugins?
361
+ def _pluginless_guardfile?
362
+ # no Reevaluator means there was no Guardfile configured that could be
363
+ # reevaluated, so we don't have a pluginless guardfile, because we don't
364
+ # have a Guardfile to begin with...
365
+ #
366
+ # But, if we have a Guardfile, we'll at least have the built-in
367
+ # Reevaluator, so the following will work:
368
+
356
369
  plugins.map(&:name) != ["reevaluator"]
357
370
  end
358
371
 
@@ -365,7 +378,7 @@ module Guard
365
378
  @watchdirs = nil
366
379
  @listener = nil
367
380
  @interactor = nil
368
- ::Guard.scope = nil
381
+ @scope = nil
369
382
  end
370
383
  end
371
384
  end
@@ -38,8 +38,18 @@ module Guard
38
38
  #
39
39
  # @return [Guard] the Guard singleton
40
40
  #
41
+
42
+ # TODO: this method has too many instance variables
43
+ # and some are mock and leak between tests,
44
+ # so ideally there should be a guard "instance"
45
+ # object that can be created anew between tests
41
46
  def setup(opts = {})
42
- _init_options(opts)
47
+ reset_options(opts)
48
+ reset_evaluator(opts)
49
+
50
+ @queue = Queue.new
51
+ @runner = ::Guard::Runner.new
52
+ @watchdirs = _setup_watchdirs
43
53
 
44
54
  ::Guard::UI.clear(force: true)
45
55
 
@@ -52,7 +62,7 @@ module Guard
52
62
  self
53
63
  end
54
64
 
55
- attr_reader :options, :evaluator, :interactor
65
+ attr_reader :options, :interactor
56
66
 
57
67
  # Used only by tests (for all I know...)
58
68
  def clear_options
@@ -85,6 +95,16 @@ module Guard
85
95
  ::Guard.scope = { groups: [], plugins: [] }
86
96
  end
87
97
 
98
+ # Used to merge CLI options with Setuper defaults
99
+ def reset_options(new_options)
100
+ @options = ::Guard::Options.new(new_options, DEFAULT_OPTIONS)
101
+ end
102
+
103
+ # TODO: code smell - too many reset_* methods
104
+ def reset_evaluator(new_options)
105
+ @evaluator = ::Guard::Guardfile::Evaluator.new(new_options)
106
+ end
107
+
88
108
  def save_scope
89
109
  # This actually replaces scope from command line,
90
110
  # so scope set by 'scope' Pry command will be reset
@@ -107,12 +127,11 @@ module Guard
107
127
  def setup_scope(scope = {})
108
128
  # TODO: there should be a special Scope class instead
109
129
  scope = _prepare_scope(scope)
110
- { groups: :add_group, plugins: :plugin }.each do |type, meth|
111
- next unless scope[type].any?
112
- ::Guard.scope[type] = scope[type].map do |item|
113
- ::Guard.send(meth, item)
114
- end
115
- end
130
+
131
+ ::Guard.scope = {
132
+ groups: scope[:groups].map { |item| ::Guard.add_group(item) },
133
+ plugins: scope[:plugins].map { |item| ::Guard.plugin(item) },
134
+ }
116
135
  end
117
136
 
118
137
  # Evaluates the Guardfile content. It displays an error message if no
@@ -123,7 +142,7 @@ module Guard
123
142
  def evaluate_guardfile
124
143
  evaluator.evaluate_guardfile
125
144
  msg = "No plugins found in Guardfile, please add at least one."
126
- ::Guard::UI.error msg unless _non_builtin_plugins?
145
+ ::Guard::UI.error msg unless _pluginless_guardfile?
127
146
  end
128
147
 
129
148
  # Asynchronously trigger changes
@@ -146,8 +165,7 @@ module Guard
146
165
  ! @queue.empty?
147
166
  end
148
167
 
149
- def add_builtin_plugins
150
- guardfile = ::Guard.evaluator.guardfile_path
168
+ def add_builtin_plugins(guardfile)
151
169
  return unless guardfile
152
170
 
153
171
  pattern = _relative_pathname(guardfile).to_s
@@ -306,14 +324,6 @@ module Guard
306
324
  end
307
325
  end
308
326
 
309
- def _init_options(opts)
310
- @queue = Queue.new
311
- @runner = ::Guard::Runner.new
312
- @evaluator = ::Guard::Guardfile::Evaluator.new(opts)
313
- @options = ::Guard::Options.new(opts, DEFAULT_OPTIONS)
314
- @watchdirs = _setup_watchdirs
315
- end
316
-
317
327
  def _reset_all
318
328
  reset_groups
319
329
  reset_plugins
@@ -332,17 +342,43 @@ module Guard
332
342
  end
333
343
 
334
344
  def _prepare_scope(scope)
345
+ fail "Guard::setup() not called!" if options.nil?
335
346
  plugins = Array(options[:plugin])
336
347
  plugins = Array(scope[:plugins] || scope[:plugin]) if plugins.empty?
337
348
 
349
+ # Convert objects to names
350
+ plugins.map! { |p| p.respond_to?(:name) ? p.name : p }
351
+
338
352
  groups = Array(options[:group])
339
353
  groups = Array(scope[:groups] || scope[:group]) if groups.empty?
340
354
 
355
+ # Convert objects to names
356
+ groups.map! { |g| g.respond_to?(:name) ? g.name : g }
357
+
341
358
  { plugins: plugins, groups: groups }
342
359
  end
343
360
 
344
- def _non_builtin_plugins?
361
+ def _pluginless_guardfile?
362
+ # no Reevaluator means there was no Guardfile configured that could be
363
+ # reevaluated, so we don't have a pluginless guardfile, because we don't
364
+ # have a Guardfile to begin with...
365
+ #
366
+ # But, if we have a Guardfile, we'll at least have the built-in
367
+ # Reevaluator, so the following will work:
368
+
345
369
  plugins.map(&:name) != ["reevaluator"]
346
370
  end
371
+
372
+ def _reset_for_tests
373
+ @options = nil
374
+ @queue = nil
375
+ @runner = nil
376
+ @evaluator = nil
377
+ @watchdirs = nil
378
+ @watchdirs = nil
379
+ @listener = nil
380
+ @interactor = nil
381
+ @scope = nil
382
+ end
347
383
  end
348
384
  end