alloy-kicker 1.1.2 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -35,4 +35,19 @@ And for fun, ghetto-autotest:
35
35
 
36
36
  == Installation
37
37
 
38
- $ sudo gem install alloy-kicker -s http://gems.github.com
38
+ $ sudo gem install alloy-kicker -s http://gems.github.com
39
+
40
+ == TODO
41
+
42
+ 15:12 sandbags: ideally i would like all events within a small window (e.g. 2s) to be batched into one
43
+ 15:12 sandbags: depending upon whether what is being reported is "file X changed" or "some files in directory X" changed
44
+ 15:12 sandbags: i forget the granularity FSEvents supports
45
+ 15:13 alloy: Yeah the window can be changed, which is a good idea
46
+ 15:13 alloy: FSEvents simply reports changes to files in dir X. nothing more
47
+ 15:14 sandbags: btw.. what is your rationale for using "!" in method names?
48
+ 15:14 alloy: So in this case we should simply keep the time at which an event occurs and then grep the dirs for files with an higher mtime
49
+ 15:14 sandbags: works for me
50
+ 15:16 alloy: Well the rationale in these cases was either it could raise something (validate) or it would run something external. But really it was just for kicks :)
51
+ 15:16 alloy: It could be cleaned up, as I agree it doesn't help much
52
+ 15:17 sandbags: i think it would be easy to grok without them
53
+ 15:17 alloy: Yeah indeed
data/Rakefile CHANGED
@@ -22,7 +22,7 @@ end
22
22
 
23
23
  Rake::TestTask.new do |t|
24
24
  t.libs << "test"
25
- t.test_files = FileList['test/*_test.rb']
25
+ t.test_files = FileList['test/**/*_test.rb']
26
26
  t.options = '-rs'
27
27
  end
28
28
 
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 1
3
- :minor: 1
4
- :patch: 2
3
+ :minor: 9
4
+ :patch: 0
data/bin/kicker CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.expand_path('../../lib/kicker', __FILE__)
4
- Kicker.run!
4
+ Kicker.run
data/lib/kicker.rb CHANGED
@@ -1,65 +1,53 @@
1
1
  $:.unshift File.expand_path('../../vendor', __FILE__)
2
2
  require 'rucola/fsevents'
3
- require 'growlnotifier/growl_helpers'
4
- require 'optparse'
3
+
4
+ require 'kicker/callback_chain'
5
+ require 'kicker/growl'
6
+ require 'kicker/options'
7
+ require 'kicker/utils'
8
+ require 'kicker/validate'
9
+
10
+ require 'kicker/recipes/could_not_handle_file'
11
+ require 'kicker/recipes/execute_cli_command'
5
12
 
6
13
  class Kicker
7
- OPTION_PARSER = lambda do |options|
8
- OptionParser.new do |opts|
9
- opts.banner = "Usage: #{$0} [options] -e [command] [paths to watch]"
10
-
11
- opts.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
12
- options[:command] = command
13
- end
14
-
15
- opts.on('--[no-]growl', 'Whether or not to use Growl. Default is to use growl.') do |growl|
16
- options[:growl] = growl
17
- end
18
-
19
- opts.on('--growl-command [COMMAND]', 'The command to execute when the Growl succeeded message is clicked.') do |command|
20
- options[:growl_command] = command
21
- end
14
+ class << self
15
+ attr_accessor :latency
16
+
17
+ def latency
18
+ @latency ||= 1.5
19
+ end
20
+
21
+ def paths
22
+ @paths ||= %w{ . }
23
+ end
24
+
25
+ def run(argv = ARGV)
26
+ load '.kick' if File.exist?('.kick')
27
+ new(parse_options(argv)).start
22
28
  end
23
29
  end
24
30
 
25
- def self.parse_options(argv)
26
- argv = argv.dup
27
- options = { :growl => true }
28
- OPTION_PARSER.call(options).parse!(argv)
29
- options[:paths] = argv
30
- options
31
- end
32
-
33
- def self.run!(argv = ARGV)
34
- new(parse_options(argv)).start
35
- end
36
-
37
- include Growl
38
- GROWL_NOTIFICATIONS = {
39
- :change => 'Change occured',
40
- :succeeded => 'Command succeeded',
41
- :failed => 'Command failed'
42
- }
43
- GROWL_DEFAULT_CALLBACK = lambda do
44
- OSX::NSWorkspace.sharedWorkspace.launchApplication('Terminal')
45
- end
46
-
47
- attr_writer :command
48
- attr_reader :paths
49
- attr_accessor :use_growl, :growl_command
31
+ attr_reader :latency, :paths, :last_event_processed_at
50
32
 
51
33
  def initialize(options)
52
- @paths = options[:paths].map { |path| File.expand_path(path) }
53
- @command = options[:command]
34
+ @paths = (options[:paths] ? options[:paths] : Kicker.paths).map { |path| File.expand_path(path) }
35
+
36
+ @latency = options[:latency] || self.class.latency
54
37
  @use_growl = options[:growl]
55
38
  @growl_command = options[:growl_command]
39
+
40
+ finished_processing!
41
+ end
42
+
43
+ def callback_chain
44
+ self.class.callback_chain
56
45
  end
57
46
 
58
47
  def start
59
48
  validate_options!
60
49
 
61
50
  log "Watching for changes on: #{@paths.join(', ')}"
62
- log "With command: #{command}"
63
51
  log ''
64
52
 
65
53
  run_watch_dog!
@@ -68,52 +56,11 @@ class Kicker
68
56
  OSX.CFRunLoopRun
69
57
  end
70
58
 
71
- def command
72
- "sh -c #{@command.inspect}"
73
- end
74
-
75
59
  private
76
60
 
77
- def validate_options!
78
- validate_paths_and_command!
79
- validate_paths_exist!
80
- end
81
-
82
- def validate_paths_and_command!
83
- if @paths.empty? && @command.nil?
84
- puts OPTION_PARSER.call(nil).help
85
- exit
86
- end
87
- end
88
-
89
- def validate_paths_exist!
90
- @paths.each do |path|
91
- unless File.exist?(path)
92
- puts "The given path `#{path}' does not exist"
93
- exit 1
94
- end
95
- end
96
- end
97
-
98
- def log(message)
99
- puts "[#{Time.now}] #{message}"
100
- end
101
-
102
- def last_command_succeeded?
103
- $?.success?
104
- end
105
-
106
- def last_command_status
107
- $?.to_i
108
- end
109
-
110
- def start_growl!
111
- Growl::Notifier.sharedInstance.register('Kicker', Kicker::GROWL_NOTIFICATIONS.values)
112
- end
113
-
114
61
  def run_watch_dog!
115
62
  dirs = @paths.map { |path| File.directory?(path) ? path : File.dirname(path) }
116
- watch_dog = Rucola::FSEvents.start_watching(*dirs) { |events| process(events) }
63
+ watch_dog = Rucola::FSEvents.start_watching(dirs, :latency => @latency) { |events| process(events) }
117
64
 
118
65
  trap('INT') do
119
66
  log "Cleaning up…"
@@ -122,30 +69,20 @@ class Kicker
122
69
  end
123
70
  end
124
71
 
125
- def process(events)
126
- events.each do |event|
127
- @paths.each do |path|
128
- return execute! if event.last_modified_file =~ /^#{path}/
129
- end
130
- end
72
+ def finished_processing!
73
+ @last_event_processed_at = Time.now
131
74
  end
132
75
 
133
- def execute!
134
- log "Change occured. Executing command:"
135
- growl(GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command') if @use_growl
136
-
137
- output = `#{command}`
138
- output.strip.split("\n").each { |line| log " #{line}" }
139
-
140
- log "Command #{last_command_succeeded? ? 'succeeded' : "failed (#{last_command_status})"}"
141
-
142
- if @use_growl
143
- if last_command_succeeded?
144
- callback = @growl_command.nil? ? GROWL_DEFAULT_CALLBACK : lambda { system(@growl_command) }
145
- growl(GROWL_NOTIFICATIONS[:succeeded], "Kicker: Command succeeded", output, &callback)
146
- else
147
- growl(GROWL_NOTIFICATIONS[:failed], "Kicker: Command failed (#{last_command_status})", output, &GROWL_DEFAULT_CALLBACK)
148
- end
76
+ def changed_files(events)
77
+ events.map do |event|
78
+ event.files.select { |file| File.mtime(file) > @last_event_processed_at }
79
+ end.flatten
80
+ end
81
+
82
+ def process(events)
83
+ unless (files = changed_files(events)).empty?
84
+ callback_chain.run(self, files)
85
+ finished_processing!
149
86
  end
150
87
  end
151
88
  end
@@ -0,0 +1,21 @@
1
+ class Kicker
2
+ class CallbackChain < Array
3
+ alias_method :append_callback, :push
4
+ alias_method :prepend_callback, :unshift
5
+
6
+ def run(kicker, files)
7
+ each do |callback|
8
+ files = callback.call(kicker, files)
9
+ break if !files.is_a?(Array) || files.empty?
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.callback_chain
15
+ @callback_chain ||= CallbackChain.new
16
+ end
17
+
18
+ def self.callback=(callback)
19
+ callback_chain.prepend_callback(callback)
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ require 'growlnotifier/growl_helpers'
2
+
3
+ class Kicker
4
+ include Growl
5
+
6
+ attr_accessor :use_growl, :growl_command
7
+
8
+ GROWL_NOTIFICATIONS = {
9
+ :change => 'Change occured',
10
+ :succeeded => 'Command succeeded',
11
+ :failed => 'Command failed'
12
+ }
13
+
14
+ GROWL_DEFAULT_CALLBACK = lambda do
15
+ OSX::NSWorkspace.sharedWorkspace.launchApplication('Terminal')
16
+ end
17
+
18
+ private
19
+
20
+ def start_growl!
21
+ Growl::Notifier.sharedInstance.register('Kicker', Kicker::GROWL_NOTIFICATIONS.values)
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ require 'optparse'
2
+
3
+ class Kicker
4
+ def self.option_parser
5
+ @option_parser ||= OptionParser.new do |opt|
6
+ opt.banner = "Usage: #{$0} [options] [paths to watch]"
7
+ end
8
+ end
9
+
10
+ OPTION_PARSER_CALLBACK = lambda do |options|
11
+ option_parser.on('--[no-]growl', 'Whether or not to use Growl. Default is to use growl.') do |growl|
12
+ options[:growl] = growl
13
+ end
14
+
15
+ option_parser.on('--growl-command [COMMAND]', 'The command to execute when the Growl succeeded message is clicked.') do |command|
16
+ options[:growl_command] = command
17
+ end
18
+
19
+ option_parser.on('-l', '--latency [FLOAT]', 'FSEvent grouping latency') do |latency|
20
+ options[:latency] = Float(latency)
21
+ end
22
+
23
+ option_parser
24
+ end
25
+
26
+ def self.parse_options(argv)
27
+ argv = argv.dup
28
+ options = { :growl => true }
29
+ OPTION_PARSER_CALLBACK.call(options).parse!(argv)
30
+ options[:paths] = argv unless argv.empty?
31
+ options
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ class Kicker
2
+ COULD_NOT_HANDLE_CALLBACK = lambda do |kicker, files|
3
+ kicker.log("Could not handle: #{files.join(', ')}")
4
+ end
5
+
6
+ self.callback = COULD_NOT_HANDLE_CALLBACK
7
+ end
@@ -0,0 +1,7 @@
1
+ class Kicker
2
+ option_parser.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
3
+ Kicker.callback = lambda do |kicker, _|
4
+ kicker.execute_command "sh -c #{command.inspect}"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ class Kicker
2
+ def execute_command(command)
3
+ log "Change occured. Executing command:"
4
+ growl(GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command') if @use_growl
5
+
6
+ output = `#{command}`
7
+ output.strip.split("\n").each { |line| log " #{line}" }
8
+
9
+ log "Command #{last_command_succeeded? ? 'succeeded' : "failed (#{last_command_status})"}"
10
+
11
+ if @use_growl
12
+ if last_command_succeeded?
13
+ callback = @growl_command.nil? ? GROWL_DEFAULT_CALLBACK : lambda { system(@growl_command) }
14
+ growl(GROWL_NOTIFICATIONS[:succeeded], "Kicker: Command succeeded", output, &callback)
15
+ else
16
+ growl(GROWL_NOTIFICATIONS[:failed], "Kicker: Command failed (#{last_command_status})", output, &GROWL_DEFAULT_CALLBACK)
17
+ end
18
+ end
19
+ end
20
+
21
+ def log(message)
22
+ puts "[#{Time.now}] #{message}"
23
+ end
24
+
25
+ private
26
+
27
+ def last_command_succeeded?
28
+ $?.success?
29
+ end
30
+
31
+ def last_command_status
32
+ $?.to_i
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ class Kicker
2
+ private
3
+
4
+ def validate_options!
5
+ validate_paths_and_command!
6
+ validate_paths_exist!
7
+ end
8
+
9
+ def validate_paths_and_command!
10
+ if callback_chain.length == 1
11
+ puts OPTION_PARSER_CALLBACK.call(nil).help
12
+ exit
13
+ end
14
+ end
15
+
16
+ def validate_paths_exist!
17
+ @paths.each do |path|
18
+ unless File.exist?(path)
19
+ puts "The given path `#{path}' does not exist"
20
+ exit 1
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,103 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Kicker, concerning its callback chain" do
4
+ it "should return the callback chain instance" do
5
+ Kicker.callback_chain.should.be.instance_of Kicker::CallbackChain
6
+ end
7
+
8
+ it "should provide a shortcut method which prepends a callback" do
9
+ Kicker.callback = lambda { :from_callback }
10
+ Kicker.callback_chain.first.call.should == :from_callback
11
+ end
12
+
13
+ it "should be accessible by an instance" do
14
+ kicker = Kicker.new({})
15
+ kicker.callback_chain.should.be Kicker.callback_chain
16
+ end
17
+ end
18
+
19
+ describe "Kicker::CallbackChain" do
20
+ it "should be a subclass of Array" do
21
+ Kicker::CallbackChain.superclass.should.be Array
22
+ end
23
+ end
24
+
25
+ describe "An instance of Kicker::CallbackChain, concerning it's API" do
26
+ before do
27
+ @chain = Kicker::CallbackChain.new
28
+
29
+ @callback1 = lambda {}
30
+ @callback2 = lambda {}
31
+ end
32
+
33
+ it "should append a callback" do
34
+ @chain << @callback1
35
+ @chain.append_callback(@callback2)
36
+
37
+ @chain.should == [@callback1, @callback2]
38
+ end
39
+
40
+ it "should prepend a callback" do
41
+ @chain << @callback1
42
+ @chain.prepend_callback(@callback2)
43
+
44
+ @chain.should == [@callback2, @callback1]
45
+ end
46
+ end
47
+
48
+ describe "An instance of Kicker::CallbackChain, when running the chain" do
49
+ before do
50
+ @kicker = Kicker.new({})
51
+
52
+ @chain = Kicker::CallbackChain.new
53
+ @result = []
54
+ end
55
+
56
+ it "should call the callbacks from first to last" do
57
+ @chain.append_callback lambda { @result << 1 }
58
+ @chain.append_callback lambda { @result << 2 }
59
+ @chain.run(@kicker, [])
60
+ @result.should == [1, 2]
61
+ end
62
+
63
+ it "should pass in the Kicker instance with each yield" do
64
+ kicker = nil
65
+ @chain.append_callback lambda { |x, _| kicker = x }
66
+ @chain.run(@kicker, [])
67
+ kicker.should.be @kicker
68
+ end
69
+
70
+ it "should pass the files array given to run to the first callback and pass the result array of that call to the next callback and so on" do
71
+ @chain.append_callback lambda { |_, files|
72
+ @result.concat(files)
73
+ %w{ /file/3 /file/4 }
74
+ }
75
+
76
+ @chain.append_callback lambda { |_, files|
77
+ @result.concat(files)
78
+ []
79
+ }
80
+
81
+ @chain.run(@kicker, %w{ /file/1 /file/2 })
82
+ @result.should == %w{ /file/1 /file/2 /file/3 /file/4 }
83
+ end
84
+
85
+ it "should halt the callback chain once an empty array is returned from a callback" do
86
+ @chain.append_callback lambda { @result << 1; [] }
87
+ @chain.append_callback lambda { @result << 2 }
88
+ @chain.run(@kicker, %w{ /file/1 /file/2 })
89
+ @result.should == [1]
90
+ end
91
+
92
+ it "should halt the callback chain if not an Array instance is returned from a callback" do
93
+ [nil, false, ''].each do |object|
94
+ @chain.clear
95
+ @result.clear
96
+
97
+ @chain.append_callback lambda { @result << 1; object }
98
+ @chain.append_callback lambda { @result << 2 }
99
+ @chain.run(@kicker, %w{ /file/1 /file/2 })
100
+ @result.should == [1]
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,75 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Kicker, when a change occurs" do
4
+ before do
5
+ Kicker.any_instance.stubs(:last_command_succeeded?).returns(true)
6
+ Kicker.any_instance.stubs(:log)
7
+ @kicker = Kicker.new({})
8
+ end
9
+
10
+ it "should store the current time as when the last change occurred" do
11
+ now = Time.now
12
+ Time.stubs(:now).returns(now)
13
+
14
+ @kicker.send(:finished_processing!)
15
+ @kicker.last_event_processed_at.should.be now
16
+ end
17
+
18
+ it "should return an array of files that have changed since the last event" do
19
+ file1 = touch('1')
20
+ file2 = touch('2')
21
+ file3 = touch('3')
22
+ file4 = touch('4')
23
+ @kicker.send(:finished_processing!)
24
+
25
+ events = [event(file1, file2), event(file3, file4)]
26
+
27
+ @kicker.send(:changed_files, events).should == []
28
+ @kicker.send(:finished_processing!)
29
+
30
+ sleep(1)
31
+ touch('2')
32
+
33
+ @kicker.send(:changed_files, events).should == [file2]
34
+ @kicker.send(:finished_processing!)
35
+
36
+ sleep(1)
37
+ touch('1')
38
+ touch('3')
39
+
40
+ @kicker.send(:changed_files, events).should == [file1, file3]
41
+ end
42
+
43
+ it "should run the callback chain with all changed files" do
44
+ files = %w{ /file/1 /file/2 }
45
+ events = [event('/file/1'), event('/file/2')]
46
+
47
+ @kicker.expects(:changed_files).with(events).returns(files)
48
+ @kicker.callback_chain.expects(:run).with(@kicker, files)
49
+ @kicker.expects(:finished_processing!)
50
+
51
+ @kicker.send(:process, events)
52
+ end
53
+
54
+ it "should not run the callback chain if there were no changed files" do
55
+ @kicker.stubs(:changed_files).returns([])
56
+ @kicker.callback_chain.expects(:run).never
57
+ @kicker.expects(:finished_processing!).never
58
+
59
+ @kicker.send(:process, [event()])
60
+ end
61
+
62
+ private
63
+
64
+ def touch(file)
65
+ file = "/tmp/kicker_test_tmp_#{file}"
66
+ `touch #{file}`
67
+ file
68
+ end
69
+
70
+ def event(*files)
71
+ event = stub('FSEvent')
72
+ event.stubs(:files).returns(files)
73
+ event
74
+ end
75
+ end
@@ -0,0 +1,133 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Kicker" do
4
+ it "should return the default paths to watch" do
5
+ Kicker.paths.should == %w{ . }
6
+ end
7
+
8
+ it "should check if a .kick file exists and if so load it before running" do
9
+ Kicker.any_instance.stubs(:start)
10
+
11
+ File.expects(:exist?).with('.kick').returns(true)
12
+ Kicker.expects(:load).with('.kick')
13
+ Kicker.run
14
+ end
15
+ end
16
+
17
+ describe "Kicker, when initializing" do
18
+ before do
19
+ @now = Time.now
20
+ Time.stubs(:now).returns(@now)
21
+
22
+ @kicker = Kicker.new(:paths => %w{ /some/dir a/relative/path })
23
+ end
24
+
25
+ it "should return the extended paths to watch" do
26
+ @kicker.paths.should == ['/some/dir', File.expand_path('a/relative/path')]
27
+ end
28
+
29
+ it "should have assigned the current time to last_event_processed_at" do
30
+ @kicker.last_event_processed_at.should == @now
31
+ end
32
+
33
+ it "should use the default paths if no paths were given" do
34
+ Kicker.new({}).paths.should == [File.expand_path('.')]
35
+ end
36
+
37
+ it "should use the default FSEvents latency if none was given" do
38
+ @kicker.latency.should == 1.5
39
+ end
40
+
41
+ it "should use the given FSEvents latency if one was given" do
42
+ Kicker.new(:latency => 3.5).latency.should == 3.5
43
+ end
44
+ end
45
+
46
+ describe "Kicker, when starting" do
47
+ before do
48
+ @kicker = Kicker.new(:paths => %w{ /some/file.rb })
49
+ @kicker.stubs(:log)
50
+ Rucola::FSEvents.stubs(:start_watching)
51
+ OSX.stubs(:CFRunLoopRun)
52
+ end
53
+
54
+ it "should show the usage banner and exit when there is no extra callback defined" do
55
+ @kicker.stubs(:validate_paths_exist!)
56
+ Kicker.stubs(:callback_chain).returns([1])
57
+
58
+ Kicker::OPTION_PARSER_CALLBACK.stubs(:call).returns(mock('OptionParser', :help => 'help'))
59
+ @kicker.expects(:puts).with("help")
60
+ @kicker.expects(:exit)
61
+
62
+ @kicker.start
63
+ end
64
+
65
+ it "should warn the user and exit if any of the given paths doesn't exist" do
66
+ @kicker.stubs(:validate_paths_and_command!)
67
+
68
+ @kicker.expects(:puts).with("The given path `/some/file.rb' does not exist")
69
+ @kicker.expects(:exit).with(1)
70
+
71
+ @kicker.start
72
+ end
73
+
74
+ it "should start a FSEvents stream with the assigned latency" do
75
+ @kicker.stubs(:validate_options!)
76
+
77
+ Rucola::FSEvents.expects(:start_watching).with(['/some'], :latency => @kicker.latency)
78
+ @kicker.start
79
+ end
80
+
81
+ it "should start a FSEvents stream which watches all paths, but the dirnames of paths if they're files" do
82
+ @kicker.stubs(:validate_options!)
83
+ File.stubs(:directory?).with('/some/file.rb').returns(false)
84
+
85
+ Rucola::FSEvents.expects(:start_watching).with(['/some'], :latency => @kicker.latency)
86
+ @kicker.start
87
+ end
88
+
89
+ it "should start a FSEvents stream with a block which calls #process with any generated events" do
90
+ @kicker.stubs(:validate_options!)
91
+
92
+ Rucola::FSEvents.expects(:start_watching).yields(['event'])
93
+ @kicker.expects(:process).with(['event'])
94
+
95
+ @kicker.start
96
+ end
97
+
98
+ it "should setup a signal handler for `INT' which stops the FSEvents stream and exits" do
99
+ @kicker.stubs(:validate_options!)
100
+
101
+ watch_dog = stub('Rucola::FSEvents')
102
+ Rucola::FSEvents.stubs(:start_watching).returns(watch_dog)
103
+
104
+ @kicker.expects(:trap).with('INT').yields
105
+ watch_dog.expects(:stop)
106
+ @kicker.expects(:exit)
107
+
108
+ @kicker.start
109
+ end
110
+
111
+ it "should start a CFRunLoop" do
112
+ @kicker.stubs(:validate_options!)
113
+
114
+ OSX.expects(:CFRunLoopRun)
115
+ @kicker.start
116
+ end
117
+
118
+ it "should register with growl if growl should be used" do
119
+ @kicker.stubs(:validate_options!)
120
+ @kicker.use_growl = true
121
+
122
+ Growl::Notifier.sharedInstance.expects(:register).with('Kicker', Kicker::GROWL_NOTIFICATIONS.values)
123
+ @kicker.start
124
+ end
125
+
126
+ it "should _not_ register with growl if growl should not be used" do
127
+ @kicker.stubs(:validate_options!)
128
+ @kicker.use_growl = false
129
+
130
+ Growl::Notifier.sharedInstance.expects(:register).never
131
+ @kicker.start
132
+ end
133
+ end
@@ -0,0 +1,25 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Kicker.parse_options" do
4
+ it "should parse the paths" do
5
+ Kicker.parse_options([])[:paths].should.be nil
6
+
7
+ Kicker.parse_options(%w{ /some/file.rb })[:paths].should == %w{ /some/file.rb }
8
+ Kicker.parse_options(%w{ /some/file.rb /a/dir /and/some/other/file.rb })[:paths].should ==
9
+ %w{ /some/file.rb /a/dir /and/some/other/file.rb }
10
+ end
11
+
12
+ it "should parse if growl shouldn't be used" do
13
+ Kicker.parse_options([])[:growl].should == true
14
+ Kicker.parse_options(%w{ --no-growl })[:growl].should == false
15
+ end
16
+
17
+ it "should parse the Growl command to use when the user clicks the Growl succeeded message" do
18
+ Kicker.parse_options(%w{ --growl-command ls })[:growl_command].should == 'ls'
19
+ end
20
+
21
+ it "should parse the latency to pass to FSEvents" do
22
+ Kicker.parse_options(%w{ -l 2.5 })[:latency].should == 2.5
23
+ Kicker.parse_options(%w{ --latency 3.5 })[:latency].should == 3.5
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ describe "Kicker, concerning the default `could not handle file' callback" do
4
+ it "should log that it could not handle the given files" do
5
+ kicker = Kicker.new({})
6
+ kicker.expects(:log).with("Could not handle: /file/1, /file/2")
7
+ Kicker.callback_chain.last.call(kicker, %w{ /file/1 /file/2 })
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ describe "Kicker, concerning the `execute a command-line' callback" do
4
+ it "should parse the command and add the callback" do
5
+ before = Kicker.callback_chain.length
6
+
7
+ Kicker.parse_options(%w{ -e ls })
8
+ Kicker.callback_chain.length.should == before + 1
9
+
10
+ Kicker.parse_options(%w{ --execute ls })
11
+ Kicker.callback_chain.length.should == before + 2
12
+ end
13
+
14
+ it "should call execute_command with the given command" do
15
+ Kicker.parse_options(%w{ -e ls })
16
+
17
+ callback = Kicker.callback_chain.first
18
+ callback.should.be.instance_of Proc
19
+
20
+ kicker = Kicker.new({})
21
+ kicker.expects(:execute_command).with('sh -c "ls"')
22
+
23
+ callback.call(kicker, %w{ /file/1 /file/2 }).should.not.be.instance_of Array
24
+ end
25
+ end
@@ -0,0 +1,74 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "A Kicker instance, concerning its utility methods" do
4
+ before do
5
+ Kicker.any_instance.stubs(:last_command_succeeded?).returns(true)
6
+ @kicker = Kicker.new(:paths => %w{ /some/dir }, :command => 'ls -l')
7
+ end
8
+
9
+ it "should print a log entry with timestamp" do
10
+ now = Time.now
11
+ Time.stubs(:now).returns(now)
12
+
13
+ @kicker.expects(:puts).with("[#{now}] the message")
14
+ @kicker.send(:log, 'the message')
15
+ end
16
+
17
+ it "should log the output of the command indented by 2 spaces and whether or not the command succeeded" do
18
+ @kicker.stubs(:`).returns("line 1\nline 2")
19
+
20
+ @kicker.expects(:log).with('Change occured. Executing command:')
21
+ @kicker.expects(:log).with(' line 1')
22
+ @kicker.expects(:log).with(' line 2')
23
+ @kicker.expects(:log).with('Command succeeded')
24
+ @kicker.execute_command('')
25
+
26
+ @kicker.stubs(:last_command_succeeded?).returns(false)
27
+ @kicker.stubs(:last_command_status).returns(123)
28
+ @kicker.expects(:log).with('Change occured. Executing command:')
29
+ @kicker.expects(:log).with(' line 1')
30
+ @kicker.expects(:log).with(' line 2')
31
+ @kicker.expects(:log).with('Command failed (123)')
32
+ @kicker.execute_command('')
33
+ end
34
+
35
+ it "should send the Growl messages with the default click callback" do
36
+ @kicker.stubs(:log)
37
+
38
+ @kicker.stubs(:`).returns("line 1\nline 2")
39
+ @kicker.use_growl = true
40
+
41
+ OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(2)
42
+
43
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
44
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
45
+ @kicker.execute_command('')
46
+
47
+ @kicker.stubs(:last_command_succeeded?).returns(false)
48
+ @kicker.stubs(:last_command_status).returns(123)
49
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
50
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
51
+ @kicker.execute_command('')
52
+ end
53
+
54
+ it "should send the Growl messages with a click callback which executes the specified growl command when succeeded" do
55
+ @kicker.stubs(:log)
56
+
57
+ @kicker.stubs(:`).returns("line 1\nline 2")
58
+ @kicker.use_growl = true
59
+ @kicker.growl_command = 'ls -l'
60
+
61
+ @kicker.expects(:system).with('ls -l').times(1)
62
+ OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(1)
63
+
64
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
65
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
66
+ @kicker.execute_command('')
67
+
68
+ @kicker.stubs(:last_command_succeeded?).returns(false)
69
+ @kicker.stubs(:last_command_status).returns(123)
70
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
71
+ @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
72
+ @kicker.execute_command('')
73
+ end
74
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alloy-kicker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eloy Duran
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-08 00:00:00 -07:00
12
+ date: 2009-06-25 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -30,8 +30,21 @@ files:
30
30
  - VERSION.yml
31
31
  - bin/kicker
32
32
  - lib/kicker.rb
33
- - test/kicker_test.rb
33
+ - lib/kicker/callback_chain.rb
34
+ - lib/kicker/growl.rb
35
+ - lib/kicker/options.rb
36
+ - lib/kicker/recipes/could_not_handle_file.rb
37
+ - lib/kicker/recipes/execute_cli_command.rb
38
+ - lib/kicker/utils.rb
39
+ - lib/kicker/validate.rb
40
+ - test/callback_chain_test.rb
41
+ - test/filesystem_change_test.rb
42
+ - test/initialization_test.rb
43
+ - test/options_test.rb
44
+ - test/recipes/could_not_handle_file_test.rb
45
+ - test/recipes/execute_cli_command_test.rb
34
46
  - test/test_helper.rb
47
+ - test/utils_test.rb
35
48
  - vendor/growlnotifier/growl.rb
36
49
  - vendor/growlnotifier/growl_helpers.rb
37
50
  - vendor/rucola/fsevents.rb
@@ -63,5 +76,11 @@ signing_key:
63
76
  specification_version: 2
64
77
  summary: A simple OS X CLI tool which uses FSEvents to run a given shell command.
65
78
  test_files:
66
- - test/kicker_test.rb
79
+ - test/callback_chain_test.rb
80
+ - test/filesystem_change_test.rb
81
+ - test/initialization_test.rb
82
+ - test/options_test.rb
83
+ - test/recipes/could_not_handle_file_test.rb
84
+ - test/recipes/execute_cli_command_test.rb
67
85
  - test/test_helper.rb
86
+ - test/utils_test.rb
data/test/kicker_test.rb DELETED
@@ -1,221 +0,0 @@
1
- require File.expand_path('../test_helper', __FILE__)
2
-
3
- describe "Kicker.parse_options" do
4
- it "should parse the paths" do
5
- Kicker.parse_options(%w{ /some/file.rb })[:paths].should == %w{ /some/file.rb }
6
- Kicker.parse_options(%w{ /some/file.rb /a/dir /and/some/other/file.rb })[:paths].should ==
7
- %w{ /some/file.rb /a/dir /and/some/other/file.rb }
8
- end
9
-
10
- it "should parse the command" do
11
- Kicker.parse_options(%w{ -e ls })[:command].should == 'ls'
12
- Kicker.parse_options(%w{ --execute ls })[:command].should == 'ls'
13
- end
14
-
15
- it "should parse if growl shouldn't be used" do
16
- Kicker.parse_options([])[:growl].should == true
17
- Kicker.parse_options(%w{ --no-growl })[:growl].should == false
18
- end
19
-
20
- it "should parse the Growl command to use when the user clicks the Growl succeeded message" do
21
- Kicker.parse_options(%w{ --growl-command ls })[:growl_command].should == 'ls'
22
- end
23
- end
24
-
25
- describe "Kicker, when initializing" do
26
- before do
27
- @kicker = Kicker.new(:paths => %w{ /some/dir a/relative/path }, :command => 'ls -l')
28
- end
29
-
30
- it "should return the extended paths to watch" do
31
- @kicker.paths.should == ['/some/dir', File.expand_path('a/relative/path')]
32
- end
33
-
34
- it "should return the command to execute once a change occurs" do
35
- @kicker.command.should == 'sh -c "ls -l"'
36
- end
37
- end
38
-
39
- describe "Kicker, when starting" do
40
- before do
41
- @kicker = Kicker.new(:paths => %w{ /some/file.rb }, :command => 'ls -l')
42
- @kicker.stubs(:log)
43
- Rucola::FSEvents.stubs(:start_watching)
44
- OSX.stubs(:CFRunLoopRun)
45
- end
46
-
47
- it "should show the usage banner and exit when there are no paths and a command" do
48
- @kicker.instance_variable_set("@paths", [])
49
- @kicker.command = nil
50
- @kicker.stubs(:validate_paths_exist!)
51
-
52
- Kicker::OPTION_PARSER.stubs(:call).returns(mock('OptionParser', :help => 'help'))
53
- @kicker.expects(:puts).with("help")
54
- @kicker.expects(:exit)
55
-
56
- @kicker.start
57
- end
58
-
59
- it "should warn the user and exit if any of the given paths doesn't exist" do
60
- @kicker.expects(:puts).with("The given path `/some/file.rb' does not exist")
61
- @kicker.expects(:exit).with(1)
62
-
63
- @kicker.start
64
- end
65
-
66
- it "should start a FSEvents stream which watches all paths, but the dirnames of paths if they're files" do
67
- @kicker.stubs(:validate_options!)
68
- File.stubs(:directory?).with('/some/file.rb').returns(false)
69
-
70
- Rucola::FSEvents.expects(:start_watching).with('/some')
71
- @kicker.start
72
- end
73
-
74
- it "should start a FSEvents stream with a block which calls #process with any generated events" do
75
- @kicker.stubs(:validate_options!)
76
-
77
- Rucola::FSEvents.expects(:start_watching).yields(['event'])
78
- @kicker.expects(:process).with(['event'])
79
-
80
- @kicker.start
81
- end
82
-
83
- it "should setup a signal handler for `INT' which stops the FSEvents stream and exits" do
84
- @kicker.stubs(:validate_options!)
85
-
86
- watch_dog = stub('Rucola::FSEvents')
87
- Rucola::FSEvents.stubs(:start_watching).returns(watch_dog)
88
-
89
- @kicker.expects(:trap).with('INT').yields
90
- watch_dog.expects(:stop)
91
- @kicker.expects(:exit)
92
-
93
- @kicker.start
94
- end
95
-
96
- it "should start a CFRunLoop" do
97
- @kicker.stubs(:validate_options!)
98
-
99
- OSX.expects(:CFRunLoopRun)
100
- @kicker.start
101
- end
102
-
103
- it "should register with growl if growl should be used" do
104
- @kicker.stubs(:validate_options!)
105
- @kicker.use_growl = true
106
-
107
- Growl::Notifier.sharedInstance.expects(:register).with('Kicker', Kicker::GROWL_NOTIFICATIONS.values)
108
- @kicker.start
109
- end
110
-
111
- it "should _not_ register with growl if growl should not be used" do
112
- @kicker.stubs(:validate_options!)
113
- @kicker.use_growl = false
114
-
115
- Growl::Notifier.sharedInstance.expects(:register).never
116
- @kicker.start
117
- end
118
- end
119
-
120
- describe "Kicker, when a change occurs" do
121
- before do
122
- Kicker.any_instance.stubs(:last_command_succeeded?).returns(true)
123
- Kicker.any_instance.stubs(:log)
124
- @kicker = Kicker.new(:paths => %w{ /some/file.rb /some/dir }, :command => 'ls -l')
125
- end
126
-
127
- it "should execute the command if a change occured to a watched path which is a file" do
128
- event = stub('Event', :last_modified_file => '/some/file.rb')
129
-
130
- @kicker.expects(:`).with(@kicker.command).returns('')
131
- @kicker.send(:process, [event])
132
- end
133
-
134
- it "should execute the command if a change occured to some file in watched path which is a directory" do
135
- event = stub('Event', :last_modified_file => '/some/dir/with/file.rb')
136
-
137
- @kicker.expects(:`).with(@kicker.command).returns('')
138
- @kicker.send(:process, [event])
139
- end
140
-
141
- it "should _not_ execute the command if a change occured to a file that isn't being watched" do
142
- event1 = stub('Event', :last_modified_file => '/some/other_file.rb')
143
- event2 = stub('Event', :last_modified_file => '/some/not/watched/dir/with/file.rb')
144
-
145
- @kicker.expects(:`).never
146
- @kicker.send(:process, [event1, event2])
147
- end
148
- end
149
-
150
- describe "Kicker, in general" do
151
- before do
152
- Kicker.any_instance.stubs(:last_command_succeeded?).returns(true)
153
- @kicker = Kicker.new(:paths => %w{ /some/dir }, :command => 'ls -l')
154
- end
155
-
156
- it "should print a log entry with timestamp" do
157
- now = Time.now
158
- Time.stubs(:now).returns(now)
159
-
160
- @kicker.expects(:puts).with("[#{now}] the message")
161
- @kicker.send(:log, 'the message')
162
- end
163
-
164
- it "should log the output of the command indented by 2 spaces and whether or not the command succeeded" do
165
- @kicker.stubs(:`).returns("line 1\nline 2")
166
-
167
- @kicker.expects(:log).with('Change occured. Executing command:')
168
- @kicker.expects(:log).with(' line 1')
169
- @kicker.expects(:log).with(' line 2')
170
- @kicker.expects(:log).with('Command succeeded')
171
- @kicker.send(:execute!)
172
-
173
- @kicker.stubs(:last_command_succeeded?).returns(false)
174
- @kicker.stubs(:last_command_status).returns(123)
175
- @kicker.expects(:log).with('Change occured. Executing command:')
176
- @kicker.expects(:log).with(' line 1')
177
- @kicker.expects(:log).with(' line 2')
178
- @kicker.expects(:log).with('Command failed (123)')
179
- @kicker.send(:execute!)
180
- end
181
-
182
- it "should send the Growl messages with the default click callback" do
183
- @kicker.stubs(:log)
184
-
185
- @kicker.stubs(:`).returns("line 1\nline 2")
186
- @kicker.use_growl = true
187
-
188
- OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(2)
189
-
190
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
191
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
192
- @kicker.send(:execute!)
193
-
194
- @kicker.stubs(:last_command_succeeded?).returns(false)
195
- @kicker.stubs(:last_command_status).returns(123)
196
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
197
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
198
- @kicker.send(:execute!)
199
- end
200
-
201
- it "should send the Growl messages with a click callback which executes the specified growl command when succeeded" do
202
- @kicker.stubs(:log)
203
-
204
- @kicker.stubs(:`).returns("line 1\nline 2")
205
- @kicker.use_growl = true
206
- @kicker.growl_command = 'ls -l'
207
-
208
- @kicker.expects(:system).with('ls -l').times(1)
209
- OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(1)
210
-
211
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
212
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
213
- @kicker.send(:execute!)
214
-
215
- @kicker.stubs(:last_command_succeeded?).returns(false)
216
- @kicker.stubs(:last_command_status).returns(123)
217
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
218
- @kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
219
- @kicker.send(:execute!)
220
- end
221
- end