kicker 2.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,77 @@
1
+ class Kicker
2
+ class CallbackChain < Array
3
+ alias_method :append_callback, :push
4
+ alias_method :prepend_callback, :unshift
5
+
6
+ def call(files)
7
+ each do |callback|
8
+ break if files.empty?
9
+ callback.call(files)
10
+ end
11
+ end
12
+ end
13
+
14
+ class << self
15
+ attr_writer :pre_process_chain
16
+ def pre_process_chain
17
+ @pre_process_chain ||= CallbackChain.new
18
+ end
19
+
20
+ attr_writer :process_chain
21
+ def process_chain
22
+ @process_chain ||= CallbackChain.new
23
+ end
24
+
25
+ attr_writer :post_process_chain
26
+ def post_process_chain
27
+ @post_process_chain ||= CallbackChain.new
28
+ end
29
+
30
+ attr_writer :full_chain
31
+ def full_chain
32
+ @full_chain ||= CallbackChain.new([pre_process_chain, process_chain, post_process_chain])
33
+ end
34
+ end
35
+
36
+ def pre_process_chain
37
+ self.class.pre_process_chain
38
+ end
39
+
40
+ def process_chain
41
+ self.class.process_chain
42
+ end
43
+
44
+ def post_process_chain
45
+ self.class.post_process_chain
46
+ end
47
+
48
+ def full_chain
49
+ self.class.full_chain
50
+ end
51
+ end
52
+
53
+ module Kernel
54
+ # Adds a handler to the pre_process chain. This chain is ran before the
55
+ # process chain and is processed from first to last.
56
+ #
57
+ # Takes a +callback+ object that responds to <tt>#call</tt>, or a block.
58
+ def pre_process(callback = nil, &block)
59
+ Kicker.pre_process_chain.append_callback(block ? block : callback)
60
+ end
61
+
62
+ # Adds a handler to the process chain. This chain is ran in between the
63
+ # pre_process and post_process chains. It is processed from first to last.
64
+ #
65
+ # Takes a +callback+ object that responds to <tt>#call</tt>, or a block.
66
+ def process(callback = nil, &block)
67
+ Kicker.process_chain.append_callback(block ? block : callback)
68
+ end
69
+
70
+ # Adds a handler to the post_process chain. This chain is ran after the
71
+ # process chain and is processed from last to first.
72
+ #
73
+ # Takes a +callback+ object that responds to <tt>#call</tt>, or a block.
74
+ def post_process(callback = nil, &block)
75
+ Kicker.post_process_chain.prepend_callback(block ? block : callback)
76
+ end
77
+ end
@@ -0,0 +1,30 @@
1
+ class Kicker
2
+ module ArrayExt
3
+ # Deletes elements from self for which the block evaluates to +true+. A new
4
+ # array is returned with those values the block returned. So basically, a
5
+ # combination of reject! and map.
6
+ #
7
+ # a = [1,2,3]
8
+ # b = a.take_and_map { |x| x * 2 if x == 2 }
9
+ # b # => [4]
10
+ # a # => [1, 3]
11
+ #
12
+ # If +flatten_and_compact+ is +true+, the result array will be flattened
13
+ # and compacted. The default is +true+.
14
+ def take_and_map(flatten_and_compact = true)
15
+ took = []
16
+ reject! do |x|
17
+ if result = yield(x)
18
+ took << result
19
+ end
20
+ end
21
+ if flatten_and_compact
22
+ took.flatten!
23
+ took.compact!
24
+ end
25
+ took
26
+ end
27
+ end
28
+ end
29
+
30
+ Array.send(:include, Kicker::ArrayExt)
@@ -0,0 +1,24 @@
1
+ require 'growlnotifier/growl_helpers'
2
+
3
+ class Kicker
4
+ class << self
5
+ include Growl
6
+ attr_accessor :use_growl, :growl_command
7
+ end
8
+
9
+ GROWL_NOTIFICATIONS = {
10
+ :change => 'Change occured',
11
+ :succeeded => 'Command succeeded',
12
+ :failed => 'Command failed'
13
+ }
14
+
15
+ GROWL_DEFAULT_CALLBACK = lambda do
16
+ OSX::NSWorkspace.sharedWorkspace.launchApplication('Terminal')
17
+ end
18
+
19
+ private
20
+
21
+ def start_growl!
22
+ Growl::Notifier.sharedInstance.register('Kicker', Kicker::GROWL_NOTIFICATIONS.values)
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ require 'optparse'
2
+
3
+ class Kicker
4
+ DONT_SHOW_RECIPES = %w{ could_not_handle_file execute_cli_command dot_kick }
5
+
6
+ def self.recipes_for_display
7
+ [RECIPES_DIR, USER_RECIPES_DIR].map do |dir|
8
+ Dir.glob("#{dir}/*.rb").map { |f| File.basename(f, '.rb') }
9
+ end.flatten - DONT_SHOW_RECIPES
10
+ end
11
+
12
+ def self.option_parser
13
+ @option_parser ||= OptionParser.new do |opt|
14
+ opt.banner = "Usage: #{$0} [options] [paths to watch]"
15
+ end
16
+ end
17
+
18
+ OPTION_PARSER_CALLBACK = lambda do |options|
19
+ option_parser.on('--[no-]growl', 'Whether or not to use Growl. Default is to use growl.') do |growl|
20
+ options[:growl] = growl
21
+ end
22
+
23
+ option_parser.on('--growl-command [COMMAND]', 'The command to execute when the Growl succeeded message is clicked.') do |command|
24
+ options[:growl_command] = command
25
+ end
26
+
27
+ option_parser.on('-l', '--latency [FLOAT]', "The time to collect file change events before acting on them. Defaults to 1 second.") do |latency|
28
+ options[:latency] = Float(latency)
29
+ end
30
+
31
+ option_parser.on('-r', '--recipe [NAME]', 'A named recipe to load.') do |recipe|
32
+ (options[:recipes] ||= []) << recipe
33
+ end
34
+
35
+ option_parser.separator " "
36
+ option_parser.separator " Available recipes:"
37
+ Kicker.recipes_for_display.each { |recipe| option_parser.separator " - #{recipe}" }
38
+
39
+ option_parser
40
+ end
41
+
42
+ def self.parse_options(argv)
43
+ argv = argv.dup
44
+ options = { :growl => true }
45
+ OPTION_PARSER_CALLBACK.call(options).parse!(argv)
46
+ options[:paths] = argv unless argv.empty?
47
+ options
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ post_process do |files|
2
+ log('')
3
+ log("Could not handle: #{files.join(', ')}")
4
+ log('')
5
+ end
@@ -0,0 +1,35 @@
1
+ module ReloadDotKick #:nodoc
2
+ class << self
3
+ def save_state
4
+ @features_before_dot_kick = $LOADED_FEATURES.dup
5
+ @chains_before_dot_kick = Kicker.full_chain.map { |c| c.dup }
6
+ end
7
+
8
+ def call(files)
9
+ if files.delete('.kick')
10
+ reset!
11
+ load '.kick'
12
+ end
13
+ end
14
+
15
+ def reset!
16
+ remove_loaded_features!
17
+ reset_chains!
18
+ end
19
+
20
+ def reset_chains!
21
+ Kicker.full_chain = nil
22
+
23
+ chains = @chains_before_dot_kick.map { |c| c.dup }
24
+ Kicker.pre_process_chain, Kicker.process_chain, Kicker.post_process_chain = *chains
25
+ end
26
+
27
+ def remove_loaded_features!
28
+ ($LOADED_FEATURES - @features_before_dot_kick).each do |feat|
29
+ $LOADED_FEATURES.delete(feat)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ process ReloadDotKick
@@ -0,0 +1,6 @@
1
+ Kicker.option_parser.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
2
+ pre_process do |files|
3
+ files.clear
4
+ execute "sh -c #{command.inspect}"
5
+ end
6
+ end
@@ -0,0 +1,39 @@
1
+ # A recipe which removes files from the files array, thus “ignoring” them.
2
+ #
3
+ # By default ignores logs, tmp, and svn and git files.
4
+ #
5
+ # See Kernel#ignore for info on how to ignore files.
6
+ module Ignore
7
+ def self.call(files) #:nodoc:
8
+ files.reject! { |file| ignores.any? { |ignore| file =~ ignore } }
9
+ end
10
+
11
+ def self.ignores #:nodoc:
12
+ @ignores ||= []
13
+ end
14
+
15
+ def self.ignore(regexp_or_string) #:nodoc:
16
+ ignores << (regexp_or_string.is_a?(Regexp) ? regexp_or_string : /^#{regexp_or_string}$/)
17
+ end
18
+ end
19
+
20
+ module Kernel
21
+ # Adds +regexp_or_string+ as an ignore rule.
22
+ #
23
+ # require 'ignore'
24
+ #
25
+ # ignore /^data\//
26
+ # ignore 'Rakefile'
27
+ #
28
+ # <em>Only available if the `ignore' recipe is required.</em>
29
+ def ignore(regexp_or_string)
30
+ Ignore.ignore(regexp_or_string)
31
+ end
32
+ end
33
+
34
+ pre_process Ignore
35
+
36
+ ignore("tmp")
37
+ ignore(/\w+\.log/)
38
+ ignore(/\.(svn|git)\//)
39
+ ignore("svn-commit.tmp")
@@ -0,0 +1,8 @@
1
+ process do |files|
2
+ test_files = files.take_and_map do |file|
3
+ if file =~ %r{^(test|public)/javascripts/(\w+?)(_test)*\.(js|html)$}
4
+ "test/javascripts/#{$2}_test.html"
5
+ end
6
+ end
7
+ execute "jstest #{test_files.join(' ')}" unless test_files.empty?
8
+ end
@@ -0,0 +1,54 @@
1
+ module Rails
2
+ # Maps +type+, for instance `models', to a test directory.
3
+ def self.type_to_test_dir(type)
4
+ case type
5
+ when "models"
6
+ "unit"
7
+ when "concerns"
8
+ "unit/concerns"
9
+ when "controllers", "views"
10
+ "functional"
11
+ when "helpers"
12
+ "unit/helpers"
13
+ end
14
+ end
15
+
16
+ # Returns an array consiting of all functional tests.
17
+ def self.all_functional_tests
18
+ Dir.glob("test/functional/**/*_test.rb")
19
+ end
20
+ end
21
+
22
+ process do |files|
23
+ test_files = files.take_and_map do |file|
24
+ case file
25
+ # Match any ruby test file and run it
26
+ when /^test\/.+_test\.rb$/
27
+ file
28
+
29
+ # Run all functional tests when routes.rb is saved
30
+ when 'config/routes.rb'
31
+ Rails.all_functional_tests
32
+
33
+ # Match lib/*
34
+ when /^(lib\/.+)\.rb$/
35
+ "test/#{$1}_test.rb"
36
+
37
+ # Match any file in app/ and map it to a test file
38
+ when %r{^app/(\w+)([\w/]*)/([\w\.]+)\.\w+$}
39
+ type, namespace, file = $1, $2, $3
40
+
41
+ if dir = Rails.type_to_test_dir(type)
42
+ if type == "views"
43
+ namespace = namespace.split('/')[1..-1]
44
+ file = "#{namespace.pop}_controller"
45
+ end
46
+
47
+ test_file = File.join("test", dir, namespace, "#{file}_test.rb")
48
+ test_file if File.exist?(test_file)
49
+ end
50
+ end
51
+ end
52
+
53
+ run_ruby_tests test_files
54
+ end
@@ -0,0 +1,71 @@
1
+ class Kicker
2
+ module Utils #:nodoc:
3
+ extend self
4
+
5
+ def execute(command)
6
+ @last_command = command
7
+
8
+ log "Change occured, executing command: #{command}"
9
+ Kicker.growl(GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured, executing command:', command) if Kicker.use_growl
10
+
11
+ output = `#{command}`
12
+ output.strip.split("\n").each { |line| log " #{line}" }
13
+
14
+ log "Command #{last_command_succeeded? ? 'succeeded' : "failed (#{last_command_status})"}"
15
+
16
+ if Kicker.use_growl
17
+ if last_command_succeeded?
18
+ callback = Kicker.growl_command.nil? ? GROWL_DEFAULT_CALLBACK : lambda { system(Kicker.growl_command) }
19
+ Kicker.growl(GROWL_NOTIFICATIONS[:succeeded], "Kicker: Command succeeded", output, &callback)
20
+ else
21
+ Kicker.growl(GROWL_NOTIFICATIONS[:failed], "Kicker: Command failed (#{last_command_status})", output, &GROWL_DEFAULT_CALLBACK)
22
+ end
23
+ end
24
+ end
25
+
26
+ def last_command
27
+ @last_command
28
+ end
29
+
30
+ def log(message)
31
+ puts "[#{Time.now}] #{message}"
32
+ end
33
+
34
+ def run_ruby_tests(tests)
35
+ execute "ruby -r #{tests.join(' -r ')} -e ''" unless tests.empty?
36
+ end
37
+
38
+ private
39
+
40
+ def last_command_succeeded?
41
+ $?.success?
42
+ end
43
+
44
+ def last_command_status
45
+ $?.to_i
46
+ end
47
+ end
48
+ end
49
+
50
+ module Kernel
51
+ # Prints a +message+ with timestamp to stdout.
52
+ def log(message)
53
+ Kicker::Utils.log(message)
54
+ end
55
+
56
+ # Executes the +command+, logs the output, and optionally growls.
57
+ def execute(command)
58
+ Kicker::Utils.execute(command)
59
+ end
60
+
61
+ # Returns the last executed command.
62
+ def last_command
63
+ Kicker::Utils.last_command
64
+ end
65
+
66
+ # A convenience method that takes an array of Ruby test files and runs them
67
+ # collectively.
68
+ def run_ruby_tests(tests)
69
+ Kicker::Utils.run_ruby_tests(tests)
70
+ end
71
+ 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 process_chain.empty? && pre_process_chain.empty?
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,150 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ describe "Kicker, concerning its callback chains" do
4
+ before do
5
+ @chains = [:pre_process_chain, :process_chain, :post_process_chain, :full_chain]
6
+ end
7
+
8
+ it "should return the callback chain instances" do
9
+ @chains.each do |chain|
10
+ Kicker.send(chain).should.be.instance_of Kicker::CallbackChain
11
+ end
12
+ end
13
+
14
+ it "should be accessible by an instance" do
15
+ kicker = Kicker.new({})
16
+
17
+ @chains.each do |chain|
18
+ kicker.send(chain).should == Kicker.send(chain)
19
+ end
20
+ end
21
+
22
+ it "should provide a shortcut method which appends a callback to the pre-process chain" do
23
+ Kicker.pre_process_chain.expects(:append_callback).with do |callback|
24
+ callback.call == :from_callback
25
+ end
26
+
27
+ pre_process { :from_callback }
28
+ end
29
+
30
+ it "should provide a shortcut method which appends a callback to the process chain" do
31
+ Kicker.process_chain.expects(:append_callback).with do |callback|
32
+ callback.call == :from_callback
33
+ end
34
+
35
+ process { :from_callback }
36
+ end
37
+
38
+ it "should provide a shortcut method which prepends a callback to the post-process chain" do
39
+ Kicker.post_process_chain.expects(:prepend_callback).with do |callback|
40
+ callback.call == :from_callback
41
+ end
42
+
43
+ post_process { :from_callback }
44
+ end
45
+
46
+ it "should have assigned the chains to the `full_chain'" do
47
+ Kicker.full_chain.length.should == 3
48
+ Kicker.full_chain.each_with_index do |chain, index|
49
+ chain.should.be Kicker.send(@chains[index])
50
+ end
51
+ end
52
+ end
53
+
54
+ describe "Kicker::CallbackChain" do
55
+ it "should be a subclass of Array" do
56
+ Kicker::CallbackChain.superclass.should.be Array
57
+ end
58
+ end
59
+
60
+ describe "An instance of Kicker::CallbackChain, concerning it's API" do
61
+ before do
62
+ @chain = Kicker::CallbackChain.new
63
+
64
+ @callback1 = lambda {}
65
+ @callback2 = lambda {}
66
+ end
67
+
68
+ it "should append a callback" do
69
+ @chain << @callback1
70
+ @chain.append_callback(@callback2)
71
+
72
+ @chain.should == [@callback1, @callback2]
73
+ end
74
+
75
+ it "should prepend a callback" do
76
+ @chain << @callback1
77
+ @chain.prepend_callback(@callback2)
78
+
79
+ @chain.should == [@callback2, @callback1]
80
+ end
81
+ end
82
+
83
+ describe "An instance of Kicker::CallbackChain, when calling the chain" do
84
+ before do
85
+ @chain = Kicker::CallbackChain.new
86
+ @result = []
87
+ end
88
+
89
+ it "should call the callbacks from first to last" do
90
+ @chain.append_callback lambda { @result << 1 }
91
+ @chain.append_callback lambda { @result << 2 }
92
+ @chain.call(%w{ file })
93
+ @result.should == [1, 2]
94
+ end
95
+
96
+ it "should pass the files array given to #call to each callback in the chain" do
97
+ array = %w{ /file/1 }
98
+
99
+ @chain.append_callback lambda { |files|
100
+ files.should.be array
101
+ files.concat(%w{ /file/2 })
102
+ }
103
+
104
+ @chain.append_callback lambda { |files|
105
+ files.should.be array
106
+ @result.concat(files)
107
+ }
108
+
109
+ @chain.call(array)
110
+ @result.should == %w{ /file/1 /file/2 }
111
+ end
112
+
113
+ it "should halt the callback chain once the given array is empty" do
114
+ @chain.append_callback lambda { |files| @result << 1; files.clear }
115
+ @chain.append_callback lambda { |files| @result << 2 }
116
+ @chain.call(%w{ /file/1 /file/2 })
117
+ @result.should == [1]
118
+ end
119
+
120
+ it "should not call any callback if the given array is empty" do
121
+ @chain.append_callback lambda { |files| @result << 1 }
122
+ @chain.call([])
123
+ @result.should == []
124
+ end
125
+
126
+ it "should work with a chain of chains as well" do
127
+ array = %w{ file }
128
+
129
+ kicker_and_files = lambda do |kicker, files|
130
+ kicker.should.be @kicker
131
+ files.should.be array
132
+ end
133
+
134
+ chain1 = Kicker::CallbackChain.new([
135
+ lambda { |files| files.should.be array; @result << 1 },
136
+ lambda { |files| files.should.be array; @result << 2 }
137
+ ])
138
+
139
+ chain2 = Kicker::CallbackChain.new([
140
+ lambda { |files| files.should.be array; @result << 3 },
141
+ lambda { |files| files.should.be array; @result << 4 }
142
+ ])
143
+
144
+ @chain.append_callback chain1
145
+ @chain.append_callback chain2
146
+
147
+ @chain.call(array)
148
+ @result.should == [1, 2, 3, 4]
149
+ end
150
+ end