kicker 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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