kicker 2.1.0 → 2.2.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.
Files changed (43) hide show
  1. data/.gitignore +10 -0
  2. data/.kick +37 -0
  3. data/README.rdoc +24 -21
  4. data/Rakefile +0 -1
  5. data/TODO.rdoc +4 -34
  6. data/VERSION +1 -0
  7. data/html/images/kikker.jpg +0 -0
  8. data/kicker.gemspec +106 -0
  9. data/lib/kicker.rb +46 -60
  10. data/lib/kicker/callback_chain.rb +4 -4
  11. data/lib/kicker/core_ext.rb +9 -1
  12. data/lib/kicker/growl.rb +54 -19
  13. data/lib/kicker/log_status_helper.rb +38 -0
  14. data/lib/kicker/options.rb +59 -34
  15. data/lib/kicker/recipes.rb +58 -0
  16. data/lib/kicker/recipes/could_not_handle_file.rb +5 -3
  17. data/lib/kicker/recipes/dot_kick.rb +17 -5
  18. data/lib/kicker/recipes/execute_cli_command.rb +1 -1
  19. data/lib/kicker/recipes/ignore.rb +8 -6
  20. data/lib/kicker/recipes/jstest.rb +7 -5
  21. data/lib/kicker/recipes/rails.rb +108 -43
  22. data/lib/kicker/recipes/ruby.rb +155 -0
  23. data/lib/kicker/utils.rb +36 -32
  24. data/test/callback_chain_test.rb +1 -1
  25. data/test/core_ext_test.rb +15 -5
  26. data/test/filesystem_change_test.rb +1 -1
  27. data/test/growl_test.rb +85 -0
  28. data/test/initialization_test.rb +25 -56
  29. data/test/log_status_helper_test.rb +56 -0
  30. data/test/options_test.rb +50 -12
  31. data/test/recipes/could_not_handle_file_test.rb +10 -0
  32. data/test/recipes/dot_kick_test.rb +1 -5
  33. data/test/recipes/execute_cli_command_test.rb +3 -3
  34. data/test/recipes/ignore_test.rb +1 -1
  35. data/test/recipes/jstest_test.rb +1 -1
  36. data/test/recipes/rails_test.rb +118 -18
  37. data/test/recipes/ruby_test.rb +154 -0
  38. data/test/recipes_test.rb +39 -0
  39. data/test/test_helper.rb +1 -1
  40. data/test/utils_test.rb +103 -48
  41. metadata +19 -6
  42. data/VERSION.yml +0 -4
  43. data/lib/kicker/validate.rb +0 -24
@@ -9,11 +9,19 @@ class Kicker
9
9
  # b # => [4]
10
10
  # a # => [1, 3]
11
11
  #
12
+ # If +pattern+ is specified then files matching the pattern will be taken.
13
+ #
14
+ # a = [ 'bar', 'foo/bar' ]
15
+ # b = a.take_and_map('*/bar') { |x| x }
16
+ # b # => ['foo/bar']
17
+ # a # => ['bar']
18
+ #
12
19
  # If +flatten_and_compact+ is +true+, the result array will be flattened
13
20
  # and compacted. The default is +true+.
14
- def take_and_map(flatten_and_compact = true)
21
+ def take_and_map(pattern = nil, flatten_and_compact = true)
15
22
  took = []
16
23
  reject! do |x|
24
+ next if pattern and !File.fnmatch?(pattern, x)
17
25
  if result = yield(x)
18
26
  took << result
19
27
  end
@@ -1,24 +1,59 @@
1
1
  require 'growlnotifier/growl_helpers'
2
2
 
3
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)
4
+ module Growl #:nodoc:
5
+ NOTIFICATIONS = {
6
+ :change => 'Change occured',
7
+ :succeeded => 'Command succeeded',
8
+ :failed => 'Command failed'
9
+ }
10
+
11
+ DEFAULT_CALLBACK = lambda do
12
+ OSX::NSWorkspace.sharedWorkspace.launchApplication('Terminal')
13
+ end
14
+
15
+ class << self
16
+ include ::Growl
17
+ attr_accessor :use, :command
18
+
19
+ Growl.use = true
20
+ Growl.command = nil
21
+
22
+ def use?
23
+ @use
24
+ end
25
+
26
+ def notifications
27
+ NOTIFICATIONS
28
+ end
29
+
30
+ def start!
31
+ ::Growl::Notifier.sharedInstance.register('Kicker', NOTIFICATIONS.values)
32
+ end
33
+
34
+ def change_occured(status)
35
+ growl(notifications[:change], 'Kicker: Executing', status.call(:growl) || status.command)
36
+ end
37
+
38
+ def command_callback
39
+ lambda { system(command) } if command
40
+ end
41
+
42
+ def result(status)
43
+ status.success? ? succeeded(status) : failed(status)
44
+ end
45
+
46
+ def succeeded(status)
47
+ callback = command_callback || DEFAULT_CALLBACK
48
+ body = status.call(:growl) || (Kicker.silent? ? '' : status.output)
49
+ growl(notifications[:succeeded], "Kicker: Success", body, &callback)
50
+ end
51
+
52
+ def failed(status)
53
+ message = "Kicker: Failed (#{status.exit_code})"
54
+ body = status.call(:growl) || (Kicker.silent? ? '' : status.output)
55
+ growl(notifications[:failed], message, body, &DEFAULT_CALLBACK)
56
+ end
57
+ end
23
58
  end
24
59
  end
@@ -0,0 +1,38 @@
1
+ class Kicker
2
+ class LogStatusHelper
3
+ attr_reader :command, :output, :exit_code
4
+
5
+ def initialize(proc, command)
6
+ @proc, @command, @output, @success = proc, command
7
+ end
8
+
9
+ def result(output, success, exit_code)
10
+ @output, @success, @exit_code = output, success, exit_code
11
+ end
12
+
13
+ def call(logger_type)
14
+ @logger_type = logger_type
15
+ @proc.call(self) if @proc
16
+ end
17
+
18
+ def stdout?
19
+ @logger_type == :stdout
20
+ end
21
+
22
+ def growl?
23
+ @logger_type == :growl
24
+ end
25
+
26
+ def before?
27
+ @output.nil?
28
+ end
29
+
30
+ def after?
31
+ !before?
32
+ end
33
+
34
+ def success?
35
+ @success
36
+ end
37
+ end
38
+ end
@@ -1,49 +1,74 @@
1
1
  require 'optparse'
2
2
 
3
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]"
4
+ class << self
5
+ attr_accessor :latency, :paths, :silent, :quiet
6
+
7
+ def silent?
8
+ @silent
9
+ end
10
+
11
+ def quiet?
12
+ @quiet
15
13
  end
16
14
  end
17
15
 
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
16
+ self.latency = 1
17
+ self.paths = %w{ . }
18
+ self.silent = false
19
+ self.quiet = false
20
+
21
+ module Options #:nodoc:
22
+ DONT_SHOW_RECIPES = %w{ could_not_handle_file execute_cli_command dot_kick }
22
23
 
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
24
+ def self.recipes_for_display
25
+ Kicker::Recipes.recipe_files.map { |f| File.basename(f, '.rb') } - DONT_SHOW_RECIPES
25
26
  end
26
27
 
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)
28
+ def self.parser
29
+ @parser ||= OptionParser.new do |opt|
30
+ opt.banner = "Usage: #{$0} [options] [paths to watch]"
31
+ opt.separator " "
32
+ opt.separator " Available recipes: #{recipes_for_display.join(", ")}."
33
+ opt.separator " "
34
+
35
+ opt.on('-s', '--silent', 'Keep output to a minimum.') do |silent|
36
+ Kicker.silent = true
37
+ end
38
+
39
+ opt.on('-q', '--quiet', "Quiet output. Don't print timestamps when logging.") do |quiet|
40
+ Kicker.silent = Kicker.quiet = true
41
+ end
42
+
43
+ opt.on('--[no-]growl', 'Whether or not to use Growl. Default is to use growl.') do |growl|
44
+ Kicker::Growl.use = growl
45
+ end
46
+
47
+ opt.on('--growl-command [COMMAND]', 'The command to execute when the Growl succeeded message is clicked.') do |command|
48
+ Kicker::Growl.command = command
49
+ end
50
+
51
+ opt.on('-l', '--latency [FLOAT]', "The time to collect file change events before acting on them. Defaults to #{Kicker.latency} second.") do |latency|
52
+ Kicker.latency = Float(latency)
53
+ end
54
+
55
+ opt.on('-r', '--recipe [NAME]', 'A named recipe to load.') do |name|
56
+ recipe(name)
57
+ end
58
+ end
29
59
  end
30
60
 
31
- option_parser.on('-r', '--recipe [NAME]', 'A named recipe to load.') do |recipe|
32
- (options[:recipes] ||= []) << recipe
61
+ def self.parse(argv)
62
+ parser.parse!(argv)
63
+ Kicker.paths = argv unless argv.empty?
33
64
  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
65
  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
66
+ end
67
+
68
+ module Kernel
69
+ # Returns the global OptionParser instance that recipes can use to add
70
+ # options.
71
+ def options
72
+ Kicker::Options.parser
48
73
  end
49
74
  end
@@ -0,0 +1,58 @@
1
+ RECIPES_DIR = File.expand_path('../recipes', __FILE__)
2
+ USER_RECIPES_DIR = File.expand_path('~/.kick')
3
+
4
+ $:.unshift(RECIPES_DIR)
5
+ $:.unshift(USER_RECIPES_DIR) if File.exist?(USER_RECIPES_DIR)
6
+
7
+ module Kernel
8
+ # If only given a <tt>name</tt>, the specified recipe will be loaded. For
9
+ # instance, the following, in a <tt>.kick</tt> file, will load the Rails
10
+ # recipe:
11
+ #
12
+ # recipe :rails
13
+ #
14
+ # However, this same method is used to define a callback that is called _if_
15
+ # the recipe is loaded. For instance, the following, in a recipe file, will
16
+ # be called if the recipe is actually used:
17
+ #
18
+ # recipe :rails do
19
+ # # Load anything needed for the recipe.
20
+ # process do
21
+ # # ...
22
+ # end
23
+ # end
24
+ def recipe(name, &block)
25
+ Kicker::Recipes.recipe(name, &block)
26
+ end
27
+ end
28
+
29
+ class Kicker
30
+ module Recipes #:nodoc:
31
+ class << self
32
+ def recipes
33
+ @recipes ||= {}
34
+ end
35
+
36
+ def recipe(name, &block)
37
+ name = name.to_sym
38
+ if block_given?
39
+ recipes[name] = block
40
+ else
41
+ if recipe = recipes[name]
42
+ recipe.call
43
+ else
44
+ raise LoadError, "Recipe `#{name}' does not exist."
45
+ end
46
+ end
47
+ end
48
+
49
+ def recipe_files
50
+ Dir.glob(File.join(RECIPES_DIR, '*.rb')) + Dir.glob(File.join(USER_RECIPES_DIR, '*.rb'))
51
+ end
52
+ end
53
+
54
+ # We don't want this option to show up at the end
55
+ require 'execute_cli_command'
56
+ recipe_files.each { |file| require file }
57
+ end
58
+ end
@@ -1,5 +1,7 @@
1
1
  post_process do |files|
2
- log('')
3
- log("Could not handle: #{files.join(', ')}")
4
- log('')
2
+ unless Kicker.silent?
3
+ log('')
4
+ log("Could not handle: #{files.join(', ')}")
5
+ log('')
6
+ end
5
7
  end
@@ -6,15 +6,21 @@ module ReloadDotKick #:nodoc
6
6
  end
7
7
 
8
8
  def call(files)
9
- if files.delete('.kick')
10
- reset!
11
- load '.kick'
12
- end
9
+ reset! if files.delete('.kick')
10
+ end
11
+
12
+ def use?
13
+ File.exist?('.kick')
14
+ end
15
+
16
+ def load!
17
+ load '.kick'
13
18
  end
14
19
 
15
20
  def reset!
16
21
  remove_loaded_features!
17
22
  reset_chains!
23
+ load!
18
24
  end
19
25
 
20
26
  def reset_chains!
@@ -32,4 +38,10 @@ module ReloadDotKick #:nodoc
32
38
  end
33
39
  end
34
40
 
35
- process ReloadDotKick
41
+ if ReloadDotKick.use?
42
+ startup do
43
+ pre_process ReloadDotKick
44
+ ReloadDotKick.save_state
45
+ ReloadDotKick.load!
46
+ end
47
+ end
@@ -1,4 +1,4 @@
1
- Kicker.option_parser.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
1
+ options.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
2
2
  callback = lambda do |files|
3
3
  files.clear
4
4
  execute "sh -c #{command.inspect}"
@@ -31,9 +31,11 @@ module Kernel
31
31
  end
32
32
  end
33
33
 
34
- pre_process Ignore
35
-
36
- ignore("tmp")
37
- ignore(/\w+\.log/)
38
- ignore(/\.(svn|git)\//)
39
- ignore("svn-commit.tmp")
34
+ recipe :ignore do
35
+ pre_process Ignore
36
+
37
+ ignore("tmp")
38
+ ignore(/\w+\.log/)
39
+ ignore(/\.(svn|git)\//)
40
+ ignore("svn-commit.tmp")
41
+ end
@@ -1,8 +1,10 @@
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"
1
+ recipe :jstest do
2
+ process do |files|
3
+ test_files = files.take_and_map do |file|
4
+ if file =~ %r{^(test|public)/javascripts/(\w+?)(_test)*\.(js|html)$}
5
+ "test/javascripts/#{$2}_test.html"
6
+ end
5
7
  end
8
+ execute "jstest #{test_files.join(' ')}" unless test_files.empty?
6
9
  end
7
- execute "jstest #{test_files.join(' ')}" unless test_files.empty?
8
10
  end
@@ -1,54 +1,119 @@
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"
1
+ # Need to define these modules, because AS breaks if these aren't defined. Need to fix that in AS...
2
+ module ActiveSupport #:nodoc:
3
+ module CoreExtensions #:nodoc:
4
+ module String #:nodoc:
5
+ module Inflections #:nodoc:
6
+ end
13
7
  end
14
8
  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
9
  end
21
10
 
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"
11
+ require 'ruby'
12
+
13
+ class Rails < Ruby
14
+ class << self
15
+ # Call these options on the Ruby class which takes the cli options.
16
+ %w{ test_type runner_bin test_cases_root test_options }.each do |delegate|
17
+ define_method(delegate) { Ruby.send(delegate) }
18
+ end
36
19
 
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"
20
+ # Maps +type+, for instance `models', to a test directory.
21
+ def type_to_test_dir(type)
22
+ if test_type == 'test'
23
+ case type
24
+ when "models"
25
+ "unit"
26
+ when "concerns"
27
+ "unit/concerns"
28
+ when "controllers", "views"
29
+ "functional"
30
+ when "helpers"
31
+ "unit/helpers"
32
+ end
33
+ elsif test_type == 'spec'
34
+ case type
35
+ when "models"
36
+ "models"
37
+ when "concerns"
38
+ "models/concerns"
39
+ when "controllers", "views"
40
+ "controllers"
41
+ when "helpers"
42
+ "helpers"
45
43
  end
46
-
47
- test_file = File.join("test", dir, namespace, "#{file}_test.rb")
48
- test_file if File.exist?(test_file)
49
44
  end
50
45
  end
46
+
47
+ # Returns an array consiting of all controller tests.
48
+ def all_controller_tests
49
+ if test_type == 'test'
50
+ Dir.glob("#{test_cases_root}/functional/**/*_test.rb")
51
+ else
52
+ Dir.glob("#{test_cases_root}/controllers/**/*_spec.rb")
53
+ end
54
+ end
55
+ end
56
+
57
+ # Returns an array of all tests related to the given model.
58
+ def tests_for_model(model)
59
+ if test_type == 'test'
60
+ %W{
61
+ unit/#{model.singularize}
62
+ unit/helpers/#{model.pluralize}_helper
63
+ functional/#{model.pluralize}_controller
64
+ }
65
+ else
66
+ %W{
67
+ models/#{model.singularize}
68
+ helpers/#{model.pluralize}_helper
69
+ controllers/#{model.pluralize}_controller
70
+ }
71
+ end.map { |f| test_file f }
72
+ end
73
+
74
+ def handle!
75
+ @tests.concat(@files.take_and_map do |file|
76
+ case file
77
+ # Run all functional tests when routes.rb is saved
78
+ when 'config/routes.rb'
79
+ Rails.all_controller_tests
80
+
81
+ # Match lib/*
82
+ when /^(lib\/.+)\.rb$/
83
+ test_file($1)
84
+
85
+ # Map fixtures to their related tests
86
+ when %r{^#{test_cases_root}/fixtures/(\w+)\.yml$}
87
+ tests_for_model($1)
88
+
89
+ # Match any file in app/ and map it to a test file
90
+ when %r{^app/(\w+)([\w/]*)/([\w\.]+)\.\w+$}
91
+ type, namespace, file = $1, $2, $3
92
+
93
+ if dir = Rails.type_to_test_dir(type)
94
+ if type == "views"
95
+ namespace = namespace.split('/')[1..-1]
96
+ file = "#{namespace.pop}_controller"
97
+ end
98
+
99
+ test_file File.join(dir, namespace, file)
100
+ end
101
+ end
102
+ end)
103
+
104
+ # And let the Ruby handler match other stuff.
105
+ super
51
106
  end
107
+ end
108
+
109
+ recipe :rails do
110
+ require 'rubygems' rescue LoadError
111
+ require 'active_support/core_ext/string'
112
+
113
+ process Rails
52
114
 
53
- run_ruby_tests test_files
115
+ # When changing the schema, prepare the test database.
116
+ process do |files|
117
+ execute 'rake db:test:prepare' if files.delete('db/schema.rb')
118
+ end
54
119
  end