kicker 2.1.0 → 2.2.0

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