Repository: alloy/kicker Branch: master Commit: 5dcd9a9e91d3 Files: 48 Total size: 82.6 KB Directory structure: gitextract_s68j9c9x/ ├── .gitignore ├── .kick ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.rdoc ├── Rakefile ├── TODO.rdoc ├── bin/ │ └── kicker ├── kicker.gemspec ├── lib/ │ ├── kicker/ │ │ ├── callback_chain.rb │ │ ├── core_ext.rb │ │ ├── fsevents.rb │ │ ├── job.rb │ │ ├── notification.rb │ │ ├── options.rb │ │ ├── recipes/ │ │ │ ├── could_not_handle_file.rb │ │ │ ├── dot_kick.rb │ │ │ ├── execute_cli_command.rb │ │ │ ├── ignore.rb │ │ │ ├── jstest.rb │ │ │ ├── rails.rb │ │ │ └── ruby.rb │ │ ├── recipes.rb │ │ ├── utils.rb │ │ └── version.rb │ └── kicker.rb ├── rakelib/ │ └── gem_release.rake └── spec/ ├── callback_chain_spec.rb ├── core_ext_spec.rb ├── filesystem_change_spec.rb ├── fixtures/ │ └── a_file_thats_reloaded.rb ├── fsevents_spec.rb ├── initialization_spec.rb ├── job_spec.rb ├── kicker_spec.rb ├── notification_spec.rb ├── options_spec.rb ├── recipes/ │ ├── could_not_handle_file_spec.rb │ ├── dot_kick_spec.rb │ ├── execute_cli_command_spec.rb │ ├── ignore_spec.rb │ ├── jstest_spec.rb │ ├── rails_spec.rb │ └── ruby_spec.rb ├── recipes_spec.rb ├── spec_helper.rb └── utils_spec.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .*.sw? *.gem /.rbenv-version .DS_Store /coverage /rdoc /pkg /html /Gemfile.lock /tmp/ /.idea ================================================ FILE: .kick ================================================ recipe :ignore recipe :ruby Kicker::Recipes::Ruby.runner_bin = 'bacon' process do |files| test_files = files.take_and_map do |file| case file when %r{^lib/kicker(\.rb|/validate\.rb|/growl\.rb)$} ["spec/initialization_spec.rb", ("spec/filesystem_change_spec.rb" if $1 == '.rb')] when %r{^lib/kicker/(.+)\.rb$} "spec/#{$1}_spec.rb" end end Kicker::Recipes::Ruby.run_tests test_files end process do |files| execute("rake docs:generate && open -a Safari html/index.html") if files.delete("README.rdoc") end startup do log "Good choice mate!" end # process do # execute "ls -l" do |status| # if status.before? # status.stdout? ? "Here we go!: #{status.command}" : "Here we go! GROWL" # elsif status.after? # if status.success? # status.stdout? ? "Nice!\n\n#{status.output}" : "Nice!" # else # status.stdout? ? "Damn brow!\n\n#{status.output}" : "Damn bro!" # end # end # end # end ================================================ FILE: .travis.yml ================================================ rvm: - 2.1.3 - 2.0.0 - 1.9.3 script: "rake" ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gemspec gem 'rake' platforms :mri_18 do gem 'rdoc' end ================================================ FILE: LICENSE ================================================ Kicker: Copyright (c) 2009 Eloy Duran Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ====================================================================== Rucola: http://github.com/alloy/rucola/tree/master Copyright (c) 2008 Eloy Duran Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ====================================================================== growlnotifier: http://github.com/psychs/growlnotifier/tree/master Copyright (c) 2007-2008 Satoshi Nakagawa , Eloy Duran You can redistribute it and/or modify it under the same terms as Ruby. ================================================ FILE: README.rdoc ================================================ = Kicker {Build Status}[https://travis-ci.org/alloy/kicker] A lean, agnostic, flexible file-change watcher. == Installation $ gem install kicker -s http://gemcutter.org == The short version Usage: ./bin/kicker [options] [paths to watch] Available recipes: ignore, jstest, rails, ruby. -s, --silent Keep output to a minimum. -q, --quiet Quiet output. Don't print timestamps when logging. -c, --clear Clear console before each run. -l, --latency [FLOAT] The time to collect file change events before acting on them. Defaults to 1 second. -r, --recipe [NAME] A named recipe to load. -e, --execute [COMMAND] The command to execute. -b, --ruby [PATH] Use an alternate Ruby binary for spawned test runners. (Default is `ruby') == The long version === Execute a shell command Show all files, whenever a change occurs in the current work directory: $ kicker -e "ls -l" . Show all files, whenever a change occurs to a specific file: $ kicker -e "ls -l" foo.txt Or use it as a ghetto-autotest, running tests whenever files change: $ kicker -e "ruby test/test_case.rb" test/test_case.rb lib/file.rb Et cetera. === Using recipes A recipe is a predefined handler. You can use as many as you like, by specifying them with the --recipe (-r) option. For instance, when in the root of a typical Ruby on Rails application, using the rails recipe will map models, concerns, controllers, helpers, and views to their respective test files. These will then all be ran with Ruby. A few recipes come shipped with Kicker: * Typical Ruby library. * Ruby on Rails, as aforementioned. * JavaScript tests, to run it needs HeadlessSquirrel[http://github.com/Fingertips/Headless-squirrel]. * Ignore, ignores logs, tmp, and svn and git files. Add your own shared recipes to ~/.kick folder or current working directory .kick. === Project specific handlers Most of the time, you’ll want to create handlers specific to the project at hand. This can be done by adding your handlers to a .kick file and running Kicker from the directory containing it. This file is reloaded once saved. No need to stop Kicker. == Writing handlers Whenever file-change events occur, Kicker will go through a chain of handlers until that the files list is empty, or the end of the chain is reached. Handlers are objects that respond to #call. These are typically Proc objects. (If you know Rack, you’re familiar with this concept.) Every handler gets passed a list of changed files and can decide whether or not to act on them. Normally when handling a file, you should remove it from the files list, unless you want to let the file fall through to another handler. In the same way, one can add files to handler to the files list. ==== Time for a simple example process do |files| execute("rake docs:generate && open -a Safari html/index.html") if files.delete("README.rdoc") end A handler is defined by passing a block to process. Which is one of three possible callback chains to add your handlers to, the others being: pre_process and post_process. See Kernel for more info. Then README.rdoc is deleted from the files array. If it did exist in the array and was deleted, a shell command is executed which runs a rake task to generate rdoc and open the docs with Safari. ==== Something more elaborate. Consider a Rails application with a mailer. Since the naming convention of mailer views tend to be fairly application specific, a specific handler has to be added: process do |files| test_files = files.take_and_map do |file| if path =~ %r{^app/views/mailer/\w+\.erb$} 'test/unit/mailer_test.rb' # elsif ... handle more app specific stuff end end Ruby.run_tests test_files end The files list is iterated over with the Array#take_and_map method, which both removes and maps the results. This is an easy way to do a common thing in recipes. See Kicker::ArrayExt for details. The handler then checks if the file is a mailer view and if so runs the mailers test case. Ruby.run_tests runs them with something like the following command: execute "ruby -r #{test_files.join(' -r ')} -e ''" unless test_files.empty? See Kernel for more info on the utility methods. To load recipes from your ~/.kick file: recipe :ignore ignore(/^data\//) That’s basically it, just remember that the order of specifying handlers _can_ be important in your decision on where to specify handlers. == Notifiers For platform specific notifications we use the notify gem. For supported backends see: https://github.com/jugyo/notify#feature. You select the notify backend by setting the NOTIFY environment variable. gem install terminal-notifier env NOTIFY=terminal-notifier kicker == Contributors * Manfred Stienstra (@manfred) * Cristi Balan (@evilchelu) * Damir Zekic (@sidonath) * Adam Keys (@therealadam) ================================================ FILE: Rakefile ================================================ require 'rdoc/task' desc "Run specs" task :spec do # shuffle to ensure that tests are run in different order files = FileList['spec/**/*_spec.rb'].shuffle sh "bundle exec bacon #{files.map { |file| "'#{file}'" }.join(' ')}" end namespace :docs do RDoc::Task.new('generate') do |t| t.main = "README.rdoc" t.rdoc_files.include("README.rdoc", "lib/**/*.rb") t.options << '--charset=utf8' end end task :docs => 'docs:generate' do FileUtils.cp_r('images', 'html') end task :default => :spec ================================================ FILE: TODO.rdoc ================================================ * Move larger parts of README to the GitHub wiki so the README is to the point. * Add a recipe which implements the basic autotest mapping API. * Make the loggers, stdout and growl, work in a chain so one can add others. This should improve portability as well, as people can easily insert growl alternatives for their platform. ================================================ FILE: bin/kicker ================================================ #!/usr/bin/env ruby if $0 == __FILE__ $:.unshift File.expand_path('../../lib', __FILE__) $:.unshift File.expand_path('../../vendor', __FILE__) require 'rubygems' end require 'kicker' Kicker.run ================================================ FILE: kicker.gemspec ================================================ # -*- encoding: utf-8 -*- $:.unshift File.expand_path('../lib', __FILE__) require 'kicker/version' require 'date' Gem::Specification.new do |s| s.name = "kicker" s.version = Kicker::VERSION s.date = Time.new s.license = 'MIT' s.summary = "A lean, agnostic, flexible file-change watcher." s.description = "Allows you to fire specific command on file-system change." s.authors = ["Eloy Duran", "Manfred Stienstra"] s.homepage = "http://github.com/alloy/kicker" s.email = %w{ eloy.de.enige@gmail.com manfred@fngtps.com } s.executables = %w{ kicker } s.require_paths = %w{ lib vendor } s.files = Dir['bin/kicker', 'lib/**/*.rb', 'README.rdoc', 'LICENSE', 'html/images/kikker.jpg'] s.extra_rdoc_files = %w{ LICENSE README.rdoc } s.add_runtime_dependency("listen", '~> 2.7.9') s.add_runtime_dependency("notify", '~> 0.5.2') s.add_development_dependency("bacon") s.add_development_dependency("mocha-on-bacon") s.add_development_dependency("activesupport") s.add_development_dependency("fakefs", '>= 0.5') end ================================================ FILE: lib/kicker/callback_chain.rb ================================================ class Kicker class CallbackChain < Array #:nodoc: alias_method :append_callback, :push alias_method :prepend_callback, :unshift def call(files, stop_when_empty = true) each do |callback| break if stop_when_empty and files.empty? callback.call(files) end end end class << self attr_writer :startup_chain def startup_chain @startup_chain ||= CallbackChain.new end attr_writer :pre_process_chain def pre_process_chain @pre_process_chain ||= CallbackChain.new end attr_writer :process_chain def process_chain @process_chain ||= CallbackChain.new end attr_writer :post_process_chain def post_process_chain @post_process_chain ||= CallbackChain.new end attr_writer :full_chain def full_chain @full_chain ||= CallbackChain.new([pre_process_chain, process_chain, post_process_chain]) end end def startup_chain self.class.startup_chain end def pre_process_chain self.class.pre_process_chain end def process_chain self.class.process_chain end def post_process_chain self.class.post_process_chain end def full_chain self.class.full_chain end end module Kernel # Adds a handler to the startup chain. This chain is ran once Kicker is done # loading _before_ starting the normal operations. Note that an empty files # array is given to the callback. # # Takes a +callback+ object that responds to #call, or a block. def startup(callback = nil, &block) Kicker.startup_chain.append_callback(block ? block : callback) end # Adds a handler to the pre_process chain. This chain is ran before the # process chain and is processed from first to last. # # Takes a +callback+ object that responds to #call, or a block. def pre_process(callback = nil, &block) Kicker.pre_process_chain.append_callback(block ? block : callback) end # Adds a handler to the process chain. This chain is ran in between the # pre_process and post_process chains. It is processed from first to last. # # Takes a +callback+ object that responds to #call, or a block. def process(callback = nil, &block) Kicker.process_chain.append_callback(block ? block : callback) end # Adds a handler to the post_process chain. This chain is ran after the # process chain and is processed from last to first. # # Takes a +callback+ object that responds to #call, or a block. def post_process(callback = nil, &block) Kicker.post_process_chain.prepend_callback(block ? block : callback) end end ================================================ FILE: lib/kicker/core_ext.rb ================================================ class Kicker module ArrayExt # Deletes elements from self for which the block evaluates to +true+. A new # array is returned with those values the block returned. So basically, a # combination of reject! and map. # # a = [1,2,3] # b = a.take_and_map { |x| x * 2 if x == 2 } # b # => [4] # a # => [1, 3] # # If +pattern+ is specified then files matching the pattern will be taken. # # a = [ 'bar', 'foo/bar' ] # b = a.take_and_map('*/bar') { |x| x } # b # => ['foo/bar'] # a # => ['bar'] # # If +flatten_and_compact+ is +true+, the result array will be flattened # and compacted. The default is +true+. def take_and_map(pattern = nil, flatten_and_compact = true) took = [] reject! do |x| next if pattern and !File.fnmatch?(pattern, x) if result = yield(x) took << result end end if flatten_and_compact took.flatten! took.compact! end took end end end Array.send(:include, Kicker::ArrayExt) ================================================ FILE: lib/kicker/fsevents.rb ================================================ # encoding: utf-8 require 'listen' class Kicker class FSEvents class FSEvent attr_reader :path def initialize(path) @path = path end def files Dir.glob("#{File.expand_path(path)}/*").map do |filename| begin [File.mtime(filename), filename] rescue Errno::ENOENT nil end end.compact.sort.reverse.map { |_, filename| filename } end end def self.start_watching(paths, options={}, &block) listener = Listen.to(*(paths.dup << options)) do |modified, added, removed| files = modified + added + removed directories = files.map { |file| File.dirname(file) }.uniq yield directories.map { |directory| Kicker::FSEvents::FSEvent.new(directory) } end listener.start listener end end end ================================================ FILE: lib/kicker/job.rb ================================================ class Kicker class Job def self.attr_with_default(name, merge_hash = false, &default) # If `nil` this returns the `default`, unless explicitely set to `nil` by # the user. define_method(name) do if instance_variable_get("@#{name}_assigned") if assigned_value = instance_variable_get("@#{name}") merge_hash ? instance_eval(&default).merge(assigned_value) : assigned_value end else instance_eval(&default) end end define_method("#{name}=") do |value| instance_variable_set("@#{name}_assigned", true) instance_variable_set("@#{name}", value) end end attr_accessor :command, :exit_code, :output def initialize(attributes) @exit_code = 0 @output = '' attributes.each { |k,v| send("#{k}=", v) } end def success? exit_code == 0 end attr_with_default(:print_before) do "Executing: #{command}" end attr_with_default(:print_after) do # Show all output if it wasn't shown before and the command fails. "\n#{output}\n\n" if Kicker.silent? && !success? end # TODO default titles?? attr_with_default(:notify_before, true) do { :title => "Kicker: Executing", :message => command } end attr_with_default(:notify_after, true) do message = Kicker.silent? ? "" : output if success? { :title => "Kicker: Success", :message => message } else { :title => "Kicker: Failed (#{exit_code})", :message => message } end end end end ================================================ FILE: lib/kicker/notification.rb ================================================ require 'notify' class Kicker module Notification #:nodoc: TITLE = 'Kicker' class << self attr_accessor :use, :app_bundle_identifier alias_method :use?, :use def notify(options) return unless use? unless message = options.delete(:message) raise "A notification requires a `:message'" end options = { :group => Dir.pwd, :activate => app_bundle_identifier }.merge(options) Notify.notify(TITLE, message, options) end end end Notification.use = ENV['NOTIFY'].to_s != '' Notification.app_bundle_identifier = 'com.apple.Terminal' end ================================================ FILE: lib/kicker/options.rb ================================================ require 'optparse' class Kicker class << self attr_accessor :latency, :paths, :silent, :quiet, :clear_console def silent? @silent end def quiet? @quiet end def clear_console? @clear_console end def osx? RUBY_PLATFORM.downcase.include?("darwin") end end self.latency = 1 self.paths = %w{ . } self.silent = false self.quiet = false self.clear_console = false module Options #:nodoc: DONT_SHOW_RECIPES = %w{ could_not_handle_file execute_cli_command dot_kick } def self.recipes_for_display Kicker::Recipes.recipe_files.map { |f| File.basename(f, '.rb') } - DONT_SHOW_RECIPES end def self.parser @parser ||= OptionParser.new do |opt| opt.banner = "Usage: #{$0} [options] [paths to watch]" opt.separator " " opt.separator " Available recipes: #{recipes_for_display.join(", ")}." opt.separator " " opt.on('-v', 'Print the Kicker version') do puts Kicker::VERSION exit end opt.on('-s', '--silent', 'Keep output to a minimum.') do |silent| Kicker.silent = true end opt.on('-q', '--quiet', "Quiet output. Don't print timestamps when logging.") do |quiet| Kicker.silent = Kicker.quiet = true end opt.on('-c', '--clear', "Clear console before each run.") do |clear| Kicker.clear_console = true end opt.on('--[no-]notification', 'Whether or not to send user notifications (on Mac OS X). Defaults to enabled.') do |notifications| Notification.use = notifications end if Kicker.osx? opt.on('--activate-app [BUNDLE ID]', "The application to activate when a notification is clicked. Defaults to `com.apple.Terminal'.") do |bundle_id| Kicker::Notification.app_bundle_identifier = bundle_id end end opt.on('-l', '--latency [FLOAT]', "The time to collect file change events before acting on them. Defaults to #{Kicker.latency} second.") do |latency| Kicker.latency = Float(latency) end opt.on('-r', '--recipe [NAME]', 'A named recipe to load.') do |name| recipe(name) end end end def self.parse(argv) parser.parse!(argv) Kicker.paths = argv unless argv.empty? end end end module Kernel # Returns the global OptionParser instance that recipes can use to add # options. def options Kicker::Options.parser end end ================================================ FILE: lib/kicker/recipes/could_not_handle_file.rb ================================================ post_process do |files| unless Kicker.silent? log('') log("Could not handle: #{files.join(', ')}") log('') end end ================================================ FILE: lib/kicker/recipes/dot_kick.rb ================================================ module ReloadDotKick #:nodoc class << self def save_state @features_before_dot_kick = $LOADED_FEATURES.dup @chains_before_dot_kick = Kicker.full_chain.map { |c| c.dup } end def call(files) reset! if files.delete('.kick') end def use? File.exist?('.kick') end def load! load '.kick' end def reset! remove_loaded_features! reset_chains! load! end def reset_chains! Kicker.full_chain = nil chains = @chains_before_dot_kick.map { |c| c.dup } Kicker.pre_process_chain, Kicker.process_chain, Kicker.post_process_chain = *chains end def remove_loaded_features! ($LOADED_FEATURES - @features_before_dot_kick).each do |feat| $LOADED_FEATURES.delete(feat) end end end end if ReloadDotKick.use? startup do pre_process ReloadDotKick ReloadDotKick.save_state ReloadDotKick.load! end end ================================================ FILE: lib/kicker/recipes/execute_cli_command.rb ================================================ options.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command| callback = lambda do |files| files.clear execute "sh -c #{command.inspect}" end startup callback pre_process callback end ================================================ FILE: lib/kicker/recipes/ignore.rb ================================================ # A recipe which removes files from the files array, thus “ignoring” them. # # By default ignores logs, tmp, and svn and git files. # # See Kernel#ignore for info on how to ignore files. module Ignore def self.call(files) #:nodoc: files.reject! { |file| ignores.any? { |ignore| file =~ ignore } } end def self.ignores #:nodoc: @ignores ||= [] end def self.ignore(regexp_or_string) #:nodoc: ignores << (regexp_or_string.is_a?(Regexp) ? regexp_or_string : /^#{regexp_or_string}$/) end end module Kernel # Adds +regexp_or_string+ as an ignore rule. # # require 'ignore' # # ignore /^data\// # ignore 'Rakefile' # # Only available if the `ignore' recipe is required. def ignore(regexp_or_string) Ignore.ignore(regexp_or_string) end end recipe :ignore do pre_process Ignore ignore("tmp") ignore(/\w+\.log/) ignore(/\.(svn|git)\//) ignore("svn-commit.tmp") end ================================================ FILE: lib/kicker/recipes/jstest.rb ================================================ recipe :jstest do process do |files| test_files = files.take_and_map do |file| if file =~ %r{^(test|public)/javascripts/(\w+?)(_test)*\.(js|html)$} "test/javascripts/#{$2}_test.html" end end execute "jstest #{test_files.join(' ')}" unless test_files.empty? end end ================================================ FILE: lib/kicker/recipes/rails.rb ================================================ recipe :ruby class Kicker::Recipes::Rails < Kicker::Recipes::Ruby class << self # Call these options on the Ruby class which takes the cli options. %w{ test_type runner_bin test_cases_root test_options }.each do |delegate| define_method(delegate) { Kicker::Recipes::Ruby.send(delegate) } end # Maps +type+, for instance `models', to a test directory. def type_to_test_dir(type) if test_type == 'test' case type when "models" "unit" when "concerns" "unit/concerns" when "controllers", "views" "functional" when "helpers" "unit/helpers" end elsif test_type == 'spec' case type when "models" "models" when "concerns" "models/concerns" when "controllers", "views" "controllers" when "helpers" "helpers" end end end # Returns an array consiting of all controller tests. def all_controller_tests if test_type == 'test' Dir.glob("#{test_cases_root}/functional/**/*_test.rb") else Dir.glob("#{test_cases_root}/controllers/**/*_spec.rb") end end end # Returns an array of all tests related to the given model. def tests_for_model(model) if test_type == 'test' %W{ unit/#{ActiveSupport::Inflector.singularize(model)} unit/helpers/#{ActiveSupport::Inflector.pluralize(model)}_helper functional/#{ActiveSupport::Inflector.pluralize(model)}_controller } else %W{ models/#{ActiveSupport::Inflector.singularize(model)} helpers/#{ActiveSupport::Inflector.pluralize(model)}_helper controllers/#{ActiveSupport::Inflector.pluralize(model)}_controller } end.map { |f| test_file f } end def handle! @tests.concat(@files.take_and_map do |file| case file # Run all functional tests when routes.rb is saved when 'config/routes.rb' Kicker::Recipes::Rails.all_controller_tests # Match lib/* when /^(lib\/.+)\.rb$/ test_file($1) # Map fixtures to their related tests when %r{^#{test_cases_root}/fixtures/(\w+)\.yml$} tests_for_model($1) # Match any file in app/ and map it to a test file when %r{^app/(\w+)([\w/]*)/([\w\.]+)\.\w+$} type, namespace, file = $1, $2, $3 if dir = Kicker::Recipes::Rails.type_to_test_dir(type) if type == "views" namespace = namespace.split('/')[1..-1] file = "#{namespace.pop}_controller" end test_file File.join(dir, namespace, file) end end end) # And let the Ruby handler match other stuff. super end end recipe :rails do require 'rubygems' rescue LoadError require 'active_support/inflector' process Kicker::Recipes::Rails # When changing the schema, prepare the test database. process do |files| execute 'rake db:test:prepare' if files.delete('db/schema.rb') end end ================================================ FILE: lib/kicker/recipes/ruby.rb ================================================ class Kicker::Recipes::Ruby class << self # Assigns the type of tests to run. Eg: `test' or `spec'. attr_writer :test_type # Returns the type of tests to run. Eg: `test' or `spec'. # # Defaults to `test' if no `spec' directory exists. def test_type @test_type ||= File.exist?('spec') ? 'spec' : 'test' end # Assigns the ruby command to run the tests with. Eg: `ruby19' or `specrb'. # # This can be set from the command line with the `-b' or `--ruby' options. attr_writer :runner_bin # Returns the ruby command to run the tests with. Eg: `ruby' or `spec'. # # Defaults to `ruby' if test_type is `test' and `spec' if test_type is # `spec'. def runner_bin @runner_bin ||= test_type == 'test' ? 'ruby' : 'rspec' end # Assigns the root directory of where test cases will be looked up. attr_writer :test_cases_root # Returns the root directory of where test cases will be looked up. # # Defaults to the value of test_type. Eg: `test' or `spec'. def test_cases_root @test_cases_root ||= test_type end attr_writer :test_options #:nodoc: # Assigns extra options that are to be passed on to the runner_bin. # # Ruby.test_options << '-I ./lib/foo' def test_options @test_options ||= [] end def reset! @test_type = nil @runner_bin = nil @test_cases_root = nil @test_options = nil end def runner_command(*parts) parts.map do |part| case part when Array part.empty? ? nil : part.join(' ') else part.to_s end end.compact.join(' ') end # Runs the given tests, if there are any, with the method defined by # test_type. If test_type is `test' the run_with_test_runner method is # used. The same applies when test_type is `spec'. def run_tests(tests) send("run_with_#{test_type}_runner", tests) unless tests.empty? end def test_runner_command(tests) tests_without_ext = tests.map { |f| f[0,f.size-3] } runner_command(runner_bin, %w{ -I. } + test_options, '-r', tests_without_ext.join(' -r '), "-e ''") end # Runs the given tests with `ruby' as unit-test tests. def run_with_test_runner(tests) execute(test_runner_command(tests)) end def spec_runner_command(tests) runner_command(runner_bin, test_options, tests) end # Runs the given tests with `spec' as RSpec tests. def run_with_spec_runner(tests) execute(spec_runner_command(tests)) end end def self.call(files) #:nodoc: handler = new(files) handler.handle! run_tests(handler.tests) end # The list of collected tests. attr_reader :tests def initialize(files) #:nodoc: @files = files @tests = [] end # A shortcut to Ruby.test_type. def test_type self.class.test_type end # A shortcut to Ruby.runner_bin. def runner_bin self.class.runner_bin end # A shortcut to Ruby.test_cases_root. def test_cases_root self.class.test_cases_root end # Returns the file for +name+ if it exists. # # test_file('foo') # => "test/foo_test.rb" # test_file('foo/bar') # => "test/foo/bar_test.rb" # test_file('does/not/exist') # => nil def test_file(name) file = File.join(test_cases_root, "#{name}_#{test_type}.rb") file if File.exist?(file) end # This method is called to collect tests. Override this if you're subclassing # and make sure to call +super+. def handle! @tests.concat(@files.take_and_map do |file| case file # Match any ruby test file when /^#{test_cases_root}\/.+_#{test_type}\.rb$/ file # A file such as ./lib/namespace/foo.rb is mapped to: # * ./test/namespace/foo_test.rb # * ./test/foo_test.rb when /^lib\/(.+)\.rb$/ if namespaced = test_file($1) namespaced elsif in_test_root = test_file(File.basename(file, '.rb')) in_test_root end end end) end end options.on('-b', '--ruby [PATH]', "Use an alternate Ruby binary for spawned test runners. (Default is `ruby')") do |command| Kicker::Recipes::Ruby.runner_bin = command end recipe :ruby do process Kicker::Recipes::Ruby # When changing the Gemfile, install dependencies process do |files| execute 'bundle install' if files.delete('Gemfile') end end ================================================ FILE: lib/kicker/recipes.rb ================================================ module Kernel # If only given a name, the specified recipe will be loaded. For # instance, the following, in a .kick file, will load the Rails # recipe: # # recipe :rails # # However, this same method is used to define a callback that is called _if_ # the recipe is loaded. For instance, the following, in a recipe file, will # be called if the recipe is actually used: # # recipe :rails do # # Load anything needed for the recipe. # process do # # ... # end # end def recipe(name, &block) Kicker::Recipes.recipe(name, &block) end end class Kicker module Recipes #:nodoc: RECIPES_DIR = Pathname.new('../recipes').expand_path(__FILE__) USER_RECIPES_DIR = Pathname.new('~/.kick').expand_path CURRENT_RECIPES_DIR = Pathname.pwd.join('.kick').expand_path RECIPES_DIRS = [RECIPES_DIR, USER_RECIPES_DIR, CURRENT_RECIPES_DIR] class << self def reset! @recipes = nil # Always load all the base recipes load_recipe :execute_cli_command load_recipe :could_not_handle_file load_recipe :dot_kick end def recipes @recipes ||= {} end def recipe_filename(name) [ USER_RECIPES_DIR, RECIPES_DIR ].each do |directory| filename = directory.join("#{name}.rb") return filename if filename.exist? end end def recipe_names recipe_files.map { |filename| filename.basename('.rb').to_s.to_sym } end def recipe_files RECIPES_DIRS.map{|dir| Pathname.glob(dir.join('*.rb')) }.flatten.uniq.map(&:expand_path) end def define_recipe(name, &block) recipes[name] = block end def load_recipe(name) if recipe_names.include?(name) load recipe_filename(name) else raise LoadError, "Can't load recipe `#{name}', it doesn't exist on disk. Loadable recipes are: #{recipe_names[0..-2].join(', ')}, and #{recipe_names[-1]}" end end def activate_recipe(name) unless recipes.has_key?(name) load_recipe(name) end if recipe = recipes[name] recipe.call else raise ArgumentError, "Can't activate the recipe `#{name}' because it hasn't been defined yet." end end # See Kernel#recipe for more information about the usage. def recipe(name, &block) name = name.to_sym if block_given? define_recipe(name, &block) else activate_recipe(name) end end end reset! end end ================================================ FILE: lib/kicker/utils.rb ================================================ require 'shellwords' if RUBY_VERSION >= "1.9" class Kicker module Utils #:nodoc: extend self attr_accessor :should_clear_screen alias_method :should_clear_screen?, :should_clear_screen def perform_work(command_or_options) if command_or_options.is_a?(Hash) options = command_or_options elsif command_or_options.is_a?(String) options = { :command => command_or_options } else raise ArgumentError, "Should be a string or a hash." end job = Job.new(options) will_execute_command(job) yield job did_execute_command(job) job end def execute(command_or_options) perform_work(command_or_options) do |job| _execute(job) yield job if block_given? end end def log(message) if Kicker.quiet puts message else now = Time.now puts "#{now.strftime('%H:%M:%S')}.#{now.usec.to_s[0,2]} | #{message}" end end def clear_console! puts(CLEAR) if Kicker.clear_console? end private CLEAR = "\e[H\e[2J" def _execute(job) silent = Kicker.silent? unless silent puts sync_before, $stdout.sync = $stdout.sync, true end output = "" popen(job.command) do |io| while str = io.read(1) output << str $stdout.print str unless silent end end job.output = output.strip job.exit_code = $?.exitstatus job ensure unless silent $stdout.sync = sync_before puts("\n\n") end end def popen(command, &block) if RUBY_VERSION >= "1.9" args = Shellwords.shellsplit(command) args << { :err => [:child, :out] } IO.popen(args, &block) else IO.popen("#{command} 2>&1", &block) end end def will_execute_command(job) puts(CLEAR) if Kicker.clear_console? && should_clear_screen? @should_clear_screen = false if message = job.print_before log(message) end if notification = job.notify_before Notification.notify(notification) end end def did_execute_command(job) if message = job.print_after puts(message) end log(job.success? ? "Success" : "Failed (#{job.exit_code})") if notification = job.notify_after Notification.notify(notification) end end end end module Kernel # Prints a +message+ with timestamp to stdout. def log(message) Kicker::Utils.log(message) end # When you perform some work (like shelling out a command to run without # using +execute+) you need to call this method, with a block in which you # perform your work, which will take care of logging the work appropriately. def perform_work(command, &block) Kicker::Utils.perform_work(command, &block) end # Executes the +command+, logs the output, and optionally sends user # notifications on Mac OS X (10.8 or higher). def execute(command, &block) Kicker::Utils.execute(command, &block) end end ================================================ FILE: lib/kicker/version.rb ================================================ class Kicker VERSION = "3.0.0" end ================================================ FILE: lib/kicker.rb ================================================ require 'kicker/version' require 'kicker/fsevents' require 'kicker/callback_chain' require 'kicker/core_ext' require 'kicker/job' require 'kicker/notification' require 'kicker/options' require 'kicker/utils' require 'kicker/recipes' class Kicker #:nodoc: def self.run(argv = ARGV) Kicker::Options.parse(argv) new.start.loop! end attr_reader :last_event_processed_at def initialize finished_processing! end def paths @paths ||= Kicker.paths.map { |path| File.expand_path(path) } end def start validate_options! log "Watching for changes on: #{paths.join(', ')}" log '' run_startup_chain run_watch_dog! self end def loop! (Thread.list - [Thread.current, Thread.main]).each(&:join) end private def validate_options! validate_paths_and_command! validate_paths_exist! end def validate_paths_and_command! if startup_chain.empty? && process_chain.empty? && pre_process_chain.empty? puts Kicker::Options.parser.help exit end end def validate_paths_exist! paths.each do |path| unless File.exist?(path) puts "The given path `#{path}' does not exist" exit 1 end end end def run_watch_dog! dirs = @paths.map { |path| File.directory?(path) ? path : File.dirname(path) } Kicker::FSEvents.start_watching(dirs, :latency => self.class.latency) do |events| process events end trap('INT') do log 'Exiting ...' exit end end def run_startup_chain startup_chain.call([], false) end def finished_processing! @last_event_processed_at = Time.now end def process(events) unless (files = changed_files(events)).empty? Utils.should_clear_screen = true full_chain.call(files) finished_processing! end end def changed_files(events) make_paths_relative(events.map do |event| files_in_directory(event.path).select { |file| file_changed_since_last_event? file } end.flatten.uniq.sort) end def files_in_directory(dir) Dir.entries(dir).sort[2..-1].map { |f| File.join(dir, f) } rescue Errno::ENOENT [] end def file_changed_since_last_event?(file) File.mtime(file) > @last_event_processed_at rescue Errno::ENOENT false end def make_paths_relative(files) return files if files.empty? wd = Dir.pwd files.map do |file| if file[0..wd.length-1] == wd file[wd.length+1..-1] else file end end end end ================================================ FILE: rakelib/gem_release.rake ================================================ NAME = 'Kicker' LOWERCASE_NAME = NAME.downcase GEM_NAME = LOWERCASE_NAME def gem_version require File.expand_path("../../lib/#{LOWERCASE_NAME}/version", __FILE__) Object.const_get(NAME).const_get('VERSION') end def gem_file "#{GEM_NAME}-#{gem_version}.gem" end desc "Build gem" task :build do sh "gem build #{GEM_NAME}.gemspec" end desc "Clean gems" task :clean do sh "rm -f *.gem" end desc "Install gem" task :install => :build do sh "gem install #{gem_file}" end desc "Clean, build, install, and push gem to rubygems.org" task :release => [:clean, :install] do sh "git tag -a #{gem_version} -m 'Release #{gem_version}'" sh "git push --tags" sh "gem push #{gem_file}" end ================================================ FILE: spec/callback_chain_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) describe "Kicker, concerning its callback chains" do before do @chains = [:startup_chain, :pre_process_chain, :process_chain, :post_process_chain, :full_chain] end it "should return the callback chain instances" do @chains.each do |chain| Kicker.send(chain).should.be.instance_of Kicker::CallbackChain end end it "should be accessible by an instance" do kicker = Kicker.new @chains.each do |chain| kicker.send(chain).should == Kicker.send(chain) end end it "should provide a shortcut method which appends a callback to the startup chain" do Kicker.startup_chain.expects(:append_callback).with do |callback| callback.call == :from_callback end startup { :from_callback } end it "should provide a shortcut method which appends a callback to the pre-process chain" do Kicker.pre_process_chain.expects(:append_callback).with do |callback| callback.call == :from_callback end pre_process { :from_callback } end it "should provide a shortcut method which appends a callback to the process chain" do Kicker.process_chain.expects(:append_callback).with do |callback| callback.call == :from_callback end process { :from_callback } end it "should provide a shortcut method which prepends a callback to the post-process chain" do Kicker.post_process_chain.expects(:prepend_callback).with do |callback| callback.call == :from_callback end post_process { :from_callback } end it "should have assigned the chains to the `full_chain' (except startup_chain)" do Kicker.full_chain.length.should == 3 Kicker.full_chain.each_with_index do |chain, index| chain.should == Kicker.send(@chains[index + 1]) end end end describe "Kicker::CallbackChain" do it "should be a subclass of Array" do Kicker::CallbackChain.superclass.should == Array end end describe "An instance of Kicker::CallbackChain, concerning it's API" do before do @chain = Kicker::CallbackChain.new @callback1 = lambda {} @callback2 = lambda {} end it "should append a callback" do @chain << @callback1 @chain.append_callback(@callback2) @chain.should == [@callback1, @callback2] end it "should prepend a callback" do @chain << @callback1 @chain.prepend_callback(@callback2) @chain.should == [@callback2, @callback1] end end describe "An instance of Kicker::CallbackChain, when calling the chain" do before do @chain = Kicker::CallbackChain.new @result = [] end it "should call the callbacks from first to last" do @chain.append_callback lambda { |files| @result << 1 } @chain.append_callback lambda { |files| @result << 2 } @chain.call(%w{ file }) @result.should == [1, 2] end it "should pass the files array given to #call to each callback in the chain" do array = %w{ /file/1 } @chain.append_callback lambda { |files| files.should == array files.concat(%w{ /file/2 }) } @chain.append_callback lambda { |files| files.should == array @result.concat(files) } @chain.call(array) @result.should == %w{ /file/1 /file/2 } end it "should halt the callback chain once the given array is empty" do @chain.append_callback lambda { |files| @result << 1; files.clear } @chain.append_callback lambda { |files| @result << 2 } @chain.call(%w{ /file/1 /file/2 }) @result.should == [1] end it "should not halt the chain if the array is empty if specified" do @chain.append_callback lambda { |files| @result << 1; files.clear } @chain.append_callback lambda { |files| @result << 2 } @chain.call(%w{ /file/1 /file/2 }, false) @result.should == [1, 2] end it "should not call any callback if the given array is empty" do @chain.append_callback lambda { |files| @result << 1 } @chain.call([]) @result.should == [] end it "should work with a chain of chains as well" do array = %w{ file } kicker_and_files = lambda do |kicker, files| kicker.should.be @kicker files.should.be array end chain1 = Kicker::CallbackChain.new([ lambda { |files| files.should == array; @result << 1 }, lambda { |files| files.should == array; @result << 2 } ]) chain2 = Kicker::CallbackChain.new([ lambda { |files| files.should == array; @result << 3 }, lambda { |files| files.should == array; @result << 4 } ]) @chain.append_callback chain1 @chain.append_callback chain2 @chain.call(array) @result.should == [1, 2, 3, 4] end end ================================================ FILE: spec/core_ext_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) describe "Array#take_and_map" do before do @array = %w{ foo bar baz foo/bar.baz foo/bar/baz } end it "should remove elements from the array for which the block evaluates to true" do @array.take_and_map { |x| x =~ /^ba/ } @array.should == %w{ foo foo/bar.baz foo/bar/baz } end it "should return a new array of the return values of each block call that evaluates to true" do @array.take_and_map { |x| $1 if x =~ /^ba(\w)/ }.should == %w{ r z } end it "should flatten and compact the result array" do @array.take_and_map do |x| x =~ /^ba/ ? %w{ f o o } : [nil] end.should == %w{ f o o f o o } end it "should not flatten and compact the result array if specified" do @array.take_and_map(nil, false) do |x| x =~ /^ba/ ? %w{ f o o } : [nil] end.should == [[nil], %w{ f o o }, %w{ f o o }, [nil], [nil]] end it "should take only files matching the pattern" do @array.take_and_map('**/*') { |x| x.reverse }.should == %w{ foo/bar.baz foo/bar/baz }.map { |s| s.reverse } end it "should not remove files not matching the pattern" do @array.take_and_map('**/*') { |x| x } @array.should == %w{ foo bar baz } end end ================================================ FILE: spec/filesystem_change_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) require 'stringio' describe "Kicker, when a change occurs" do def silent(&block) stdout = $stdout $stdout = StringIO.new yield ensure $stdout = stdout end def touch(file) file = "/tmp/kicker_test_tmp_#{file}" `touch #{file}` file end def event(*files) event = stub('FSEvent') event.stubs(:path).returns('/tmp') event end def remove_tmp_files! Dir.glob("/tmp/kicker_test_tmp_*").each { |f| File.delete(f) } end before do remove_tmp_files! Kicker::Notification.stubs(:`) Kicker.any_instance.stubs(:last_command_succeeded?).returns(true) Kicker.any_instance.stubs(:log) @kicker = Kicker.new end it "should store the current time as when the last change occurred" do now = Time.now Time.stubs(:now).returns(now) @kicker.send(:finished_processing!) @kicker.last_event_processed_at.should == now end it "should return an array of files that have changed since the last event" do file1 = touch('1') file2 = touch('2') file3 = touch('3') file4 = touch('4') @kicker.send(:finished_processing!) events = [event(file1, file2), event(file3, file4)] @kicker.send(:changed_files, events).should == [] @kicker.send(:finished_processing!) sleep(1) touch('2') @kicker.send(:changed_files, events).should == [file2] @kicker.send(:finished_processing!) sleep(1) touch('1') touch('3') @kicker.send(:changed_files, events).should == [file1, file3] end it "should return an empty array when a directory doesn't exist while collecting the files in it" do @kicker.send(:files_in_directory, '/does/not/exist').should == [] end it "should not break when determining changed files from events with missing files" do file1 = touch('1') file2 = touch('2') @kicker.send(:finished_processing!) sleep(1) touch('2') events = [event(file1, file2), event('/does/not/exist')] @kicker.send(:changed_files, events).should == [file2] end it "should return relative file paths if the path is relative to the current work dir" do sleep(1) file = touch('1') Dir.stubs(:pwd).returns('/tmp') @kicker.send(:changed_files, [event(file)]).should == [File.basename(file)] end it "should call the full_chain with all changed files" do files = %w{ /file/1 /file/2 } events = [event('/file/1'), event('/file/2')] @kicker.expects(:changed_files).with(events).returns(files) @kicker.full_chain.expects(:call).with(files) @kicker.expects(:finished_processing!) @kicker.send(:process, events) end it "should not call the full_chain if there were no changed files" do @kicker.stubs(:changed_files).returns([]) @kicker.full_chain.expects(:call).never @kicker.expects(:finished_processing!).never @kicker.send(:process, [event()]) end it "should not break when directory entries are not sorted" do sleep(1) file = touch('1') Dir.stubs(:entries).returns([File.basename(file), ".", ".."]) @kicker.send(:changed_files, [event(file)]).should == [file] end it "clears the console only once during running the chain" do Kicker.clear_console = true Kicker.silent = true Kicker::Utils.stubs(:log) Kicker::Utils.expects(:puts).with("\e[H\e[2J").once files = %w{ /file/1 /file/2 } @kicker.stubs(:changed_files).returns(files) @kicker.process_chain.append_callback lambda { |files| Kicker::Utils.perform_work('ls -l') {} } @kicker.process_chain.append_callback lambda { |files| Kicker::Utils.perform_work('ls -l') {} } silent do @kicker.send(:process, files.map { |f| event(f) }) end end it "does not clear the console if no work is ever performed" do Kicker.clear_console = true Kicker::Utils.expects(:puts).with("\e[H\e[2J").never files = %w{ /file/1 /file/2 } @kicker.stubs(:changed_files).returns(files) @kicker.full_chain.stubs(:call) @kicker.send(:process, files.map { |f| event(f) }) end end ================================================ FILE: spec/fixtures/a_file_thats_reloaded.rb ================================================ $FROM_RELOADED_FILE ||= 0 $FROM_RELOADED_FILE += 1 ================================================ FILE: spec/fsevents_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) class FakeListener def initialize(paths, options={}) @paths = paths end def change(&block) @block = block self end def start blocking=true self end def fake_event(paths) @block.call(paths, [], []) end end describe "Kicker::FSEvents" do it "calls the provided block with changed directories wrapped in an event instance" do tmp = Pathname.new('tmp').join('test') test = tmp.join('what') test.mkpath FileUtils.touch(tmp.join('file')) watch_dog = Kicker::FSEvents.start_watching([tmp.to_s]) { |e| events = e } Kicker::FSEvents::FSEvent.expects(:new).with(File.expand_path(test.to_s)) FileUtils.touch(test.join('file')) sleep 1 end end describe "Kicker::FSEvents::FSEvent" do it "returns the files from the changed directory ordered by mtime and filename" do fsevent = Kicker::FSEvents::FSEvent.new(File.expand_path('../fixtures', __FILE__)) fsevent.files.should == [File.expand_path('../fixtures/a_file_thats_reloaded.rb', __FILE__)] end end ================================================ FILE: spec/initialization_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) module ReloadDotKick; end describe "Kicker" do before do Kicker.any_instance.stubs(:start) end it "should return the default paths to watch" do Kicker.paths.should == %w{ . } end it "should default the FSEvents latency to 1" do Kicker.latency.should == 1 end end describe "Kicker, when initializing" do after do Kicker.paths = %w{ . } end it "should return the extended paths to watch" do Kicker.paths = %w{ /some/dir a/relative/path } Kicker.new.paths.should == ['/some/dir', File.expand_path('a/relative/path')] end it "should have assigned the current time to last_event_processed_at" do now = Time.now; Time.stubs(:now).returns(now) Kicker.new.last_event_processed_at.should == now end it "should use the default paths if no paths were given" do Kicker.new.paths.should == [File.expand_path('.')] end end describe "Kicker, when starting" do before do Kicker.paths = %w{ /some/file.rb } @kicker = Kicker.new @kicker.stubs(:log) @kicker.startup_chain.stubs(:call) Kicker::FSEvents.stubs(:start_watching) end after do Kicker.latency = 1 Kicker.paths = %w{ . } end it "should show the usage banner and exit when there are no callbacks defined at all" do @kicker.stubs(:validate_paths_exist!) Kicker.stubs(:startup_chain).returns(Kicker::CallbackChain.new) Kicker.stubs(:process_chain).returns(Kicker::CallbackChain.new) Kicker.stubs(:pre_process_chain).returns(Kicker::CallbackChain.new) Kicker::Options.stubs(:parser).returns(mock('OptionParser', :help => 'help')) @kicker.expects(:puts).with("help") @kicker.expects(:exit) @kicker.start end it "should warn the user and exit if any of the given paths doesn't exist" do @kicker.stubs(:validate_paths_and_command!) @kicker.expects(:puts).with("The given path `/some/file.rb' does not exist") @kicker.expects(:exit).with(1) @kicker.start end it "should start a FSEvents stream with the assigned latency" do @kicker.stubs(:validate_options!) Kicker.latency = 2.34 Kicker::FSEvents.expects(:start_watching).with(['/some'], :latency => 2.34) @kicker.start end it "should start a FSEvents stream which watches all paths, but the dirnames of paths if they're files" do @kicker.stubs(:validate_options!) File.stubs(:directory?).with('/some/file.rb').returns(false) Kicker::FSEvents.expects(:start_watching).with(['/some'], :latency => Kicker.latency) @kicker.start end it "should start a FSEvents stream with a block which calls #process with any generated events" do @kicker.stubs(:validate_options!) Kicker::FSEvents.expects(:start_watching).yields(['event']) @kicker.expects(:process).with(['event']) @kicker.start end it "should setup a signal handler for `INT' which stops the FSEvents stream and exits" do @kicker.stubs(:validate_options!) Kicker::FSEvents.stubs(:start_watching).returns(stub) @kicker.expects(:trap).with('INT').yields @kicker.expects(:exit) @kicker.start end it "should call the startup chain" do @kicker.stubs(:validate_options!) @kicker.startup_chain.expects(:call).with([], false) @kicker.start end end ================================================ FILE: spec/job_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) describe "Kicker::Job" do before do @job = Kicker::Job.new(:command => 'ls -l', :exit_code => 0, :output => "line 1\nline2") end after do Kicker.silent = true end it "initializes with an options hash" do @job.command.should == 'ls -l' @job.exit_code.should == 0 @job.output.should == "line 1\nline2" end it "returns wether or not the job was a success" do @job.should.be.success @job.exit_code = 123 @job.should.not.be.success end describe "concerning the default print and notification messages" do describe "for before a command is executed" do it "returns what command will be executed (for print)" do @job.print_before.should == 'Executing: ls -l' end it "returns what command will be executed (for notification)" do Kicker.silent = false @job.notify_before.should == { :title => 'Kicker: Executing', :message => 'ls -l' } end # TODO what if the user *does* want to send a notification? #it "does not send a notification about what command will be executed if Kicker is silent" do #Kicker.silent = true #@job.notify_before.should == nil #end end describe "for after a command is executed" do describe "for print" do it "does not return the output if the output has already been logged" do Kicker.silent = false @job.exit_code = 123 @job.print_after.should == nil end it "does not return the output if the command succeeded" do Kicker.silent = true @job.exit_code = 0 @job.print_after.should == nil end it "returns all output if it wasn't printed before and the command failed" do Kicker.silent = true @job.exit_code = 123 @job.print_after.should == "\nline 1\nline2\n\n" end end describe "for notification" do it "returns the status of the command and its output" do Kicker.silent = false @job.exit_code = 0 @job.notify_after.should == { :title => 'Kicker: Success', :message => "line 1\nline2" } @job.exit_code = 123 @job.notify_after.should == { :title => 'Kicker: Failed (123)', :message => "line 1\nline2" } end it "never returns the output if Kicker is silent" do Kicker.silent = true @job.exit_code = 0 @job.notify_after.should == { :title => 'Kicker: Success', :message => '' } @job.exit_code = 123 @job.notify_after.should == { :title => 'Kicker: Failed (123)', :message => '' } end end end end describe "concerning explicit print and notification messages" do before { Kicker.silent = false } it "returns `nil' if that was explicitely assigned" do %w{ print_before print_after notify_before notify_after }.each do |attr| @job.send("#{attr}=", nil) @job.send(attr).should == nil end end it "returns the assigned message when explicitely assigned" do @job.print_before = 'BEFORE' @job.print_before.should == 'BEFORE' @job.print_after = 'AFTER' @job.print_after.should == 'AFTER' end it "merges the assigned notification options with the default ones" do @job.notify_before = { :message => 'Checking file list' } @job.notify_before.should == { :title => 'Kicker: Executing', :message => 'Checking file list' } @job.notify_after = { :title => 'OMG' } @job.notify_after.should == {:title => 'OMG', :message => "line 1\nline2" } end end end ================================================ FILE: spec/kicker_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) require 'stringio' describe "Kicker" do before { @stdout = $stdout } after { $stdout = @stdout } it "should start" do $stdout = StringIO.new thread = Thread.new { Kicker.run([]) } thread.abort_on_exception = true sleep 5 thread.alive?.should == true thread.exit end end ================================================ FILE: spec/notification_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) describe "Kicker::Notification" do it "sends a notification, grouped by the project (identified by the working dir)" do Kicker::Notification.stubs(:use?).returns(true) Notify.expects(:notify) Kicker::Notification.notify(:title => 'Kicker: Executing', :message => 'ls -l') end it "does not send a notification if notifying is disabled" do Kicker::Notification.stubs(:use?).returns(false) Notify.expects(:notify).never Kicker::Notification.notify(:title => 'Kicker: Executing', :message => 'ls -l') end end ================================================ FILE: spec/options_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) describe "Kicker::Options.parse" do after do Kicker.latency = 1 Kicker.paths = %w{ . } Kicker.silent = false Kicker.quiet = false Kicker.clear_console = false Kicker::Notification.use = true Kicker::Notification.app_bundle_identifier = 'com.apple.Terminal' end it "should parse the paths" do Kicker::Options.parse([]) Kicker.paths.should == %w{ . } Kicker::Options.parse(%w{ /some/file.rb }) Kicker.paths.should == %w{ /some/file.rb } Kicker::Options.parse(%w{ /some/file.rb /a/dir /and/some/other/file.rb }) Kicker.paths.should == %w{ /some/file.rb /a/dir /and/some/other/file.rb } end it "parses wether or not user notifications should be used" do Kicker::Options.parse([]) Kicker::Notification.should.use Kicker::Options.parse(%w{ --no-notification }) Kicker::Notification.should.not.use end it "should parse if we should keep output to a minimum" do Kicker::Options.parse([]) Kicker.should.not.be.silent Kicker::Options.parse(%w{ -s }) Kicker.should.be.silent end it 'should parse whether or not to run in quiet mode and enable silent mode if quiet' do Kicker::Options.parse([]) Kicker.should.not.be.quiet Kicker.should.not.be.silent Kicker::Options.parse(%w{ --quiet }) Kicker.should.be.quiet Kicker.should.be.silent end it "should parse whether or not to clear the console before running" do Kicker::Options.parse([]) Kicker.should.not.clear_console Kicker::Options.parse(%w{ --clear }) Kicker.should.clear_console end if Kicker.osx? it "parses the application to activate when a user notification is clicked" do Kicker::Options.parse(%w{ --activate-app com.apple.Safari }) Kicker::Notification.app_bundle_identifier.should == 'com.apple.Safari' end end it "should parse the latency to pass to FSEvents" do Kicker::Options.parse(%w{ -l 2.5 }) Kicker.latency.should == 2.5 Kicker::Options.parse(%w{ --latency 3.5 }) Kicker.latency.should == 3.5 end it "should parse recipe requires" do Kicker::Recipes.expects(:recipe).with('rails') Kicker::Recipes.expects(:recipe).with('jstest') Kicker::Options.parse(%w{ -r rails --recipe jstest }) end end ================================================ FILE: spec/recipes/could_not_handle_file_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) describe "Kicker, concerning the default `could not handle file' callback" do before do Kicker.silent = false end it "should log that it could not handle the given files" do Kicker::Utils.expects(:log).with('') Kicker::Utils.expects(:log).with("Could not handle: /file/1, /file/2") Kicker::Utils.expects(:log).with('') Kicker.post_process_chain.last.call(%w{ /file/1 /file/2 }) end it "should not log in silent mode" do Kicker.silent = true Kicker::Utils.expects(:log).never Kicker.post_process_chain.last.call(%w{ /file/1 /file/2 }) end end ================================================ FILE: spec/recipes/dot_kick_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) describe "The .kick handler" do it "should reset $LOADED_FEATURES and callback chains to state before loading .kick and reload .kick" do ReloadDotKick.save_state features_before_dot_kick = $LOADED_FEATURES.dup chains_before_dot_kick = Kicker.full_chain.map { |c| c.dup } ReloadDotKick.expects(:load).with('.kick').twice 2.times do require File.expand_path('../../fixtures/a_file_thats_reloaded', __FILE__) process {} ReloadDotKick.call(%w{ .kick }) end $FROM_RELOADED_FILE.should == 2 $LOADED_FEATURES.should == features_before_dot_kick Kicker.full_chain.should == chains_before_dot_kick end end ================================================ FILE: spec/recipes/execute_cli_command_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) describe "Kicker, concerning the `execute a command-line' callback" do it "should parse the command and add the callback" do before = Kicker.pre_process_chain.length Kicker::Options.parse(%w{ -e ls }) Kicker.pre_process_chain.length.should == before + 1 Kicker::Options.parse(%w{ --execute ls }) Kicker.pre_process_chain.length.should == before + 2 end it "should call execute with the given command" do Kicker::Options.parse(%w{ -e ls }) callback = Kicker.pre_process_chain.last callback.should.be.instance_of Proc Kicker::Utils.expects(:execute).with('sh -c "ls"') callback.call(%w{ /file/1 /file/2 }).should.not.be.instance_of Array end it "should clear the files array to halt the chain" do Kicker::Utils.stubs(:execute) files = %w{ /file/1 /file/2 } Kicker.pre_process_chain.last.call(files) files.should.be.empty end it "should run the command directly once Kicker is done loading" do callback = Kicker.pre_process_chain.last Kicker.startup_chain.should.include callback end end ================================================ FILE: spec/recipes/ignore_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) recipe :ignore IGNORE = Kicker.pre_process_chain.find{|callback| callback == Ignore } describe "The Ignore handler" do it "should remove files that match the given regexp" do ignore(/^fo{2}bar/) files = %w{ Rakefile foobar foobarbaz } IGNORE.call(files) files.should == %w{ Rakefile } end it "should remove files that match the given string" do ignore('bazbla') files = %w{ Rakefile bazbla bazblabla } IGNORE.call(files) files.should == %w{ Rakefile bazblabla } end it "should ignore a few file types by default" do files = %w{ Rakefile foo/bar/dev.log .svn/foo svn-commit.tmp .git/foo tmp } IGNORE.call(files) files.should == %w{ Rakefile } end end ================================================ FILE: spec/recipes/jstest_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) before = Kicker.process_chain.dup recipe :jstest JSTEST = (Kicker.process_chain - before).first describe "The HeadlessSquirrel handler" do before do @files = %w{ Rakefile } end it "should match any test case files" do @files += %w{ test/javascripts/ui_test.html test/javascripts/admin_test.js } Kicker::Utils.expects(:execute). with("jstest test/javascripts/ui_test.html test/javascripts/admin_test.html") JSTEST.call(@files) @files.should == %w{ Rakefile } end it "should map public/javascripts libs to test/javascripts" do @files += %w{ public/javascripts/ui.js public/javascripts/admin.js } Kicker::Utils.expects(:execute). with("jstest test/javascripts/ui_test.html test/javascripts/admin_test.html") JSTEST.call(@files) @files.should == %w{ Rakefile } end end ================================================ FILE: spec/recipes/rails_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) recipe :rails class Kicker::Recipes::Rails class << self attr_accessor :tests_ran def run_tests(tests) self.tests_ran ||= [] self.tests_ran << tests end end end describe "The Rails handler" do it "should return all controller tests when test_type is `test'" do tests = %w{ test.rb } File.use_original_exist = false File.existing_files = tests Kicker::Recipes::Ruby.test_type = 'test' Kicker::Recipes::Ruby.test_cases_root = nil Dir.expects(:glob).with("test/functional/**/*_test.rb").returns(tests) Kicker::Recipes::Rails.all_controller_tests.should == tests end it "should return all controller tests when test_type is `spec'" do specs = %w{ spec.rb } File.use_original_exist = false File.existing_files = specs Kicker::Recipes::Ruby.test_type = 'spec' Kicker::Recipes::Ruby.test_cases_root = nil Dir.expects(:glob).with("spec/controllers/**/*_spec.rb").returns(specs) Kicker::Recipes::Rails.all_controller_tests.should == specs end end describe "The Rails schema handler" do before do # We assume the Rails schema handler is in the chain after the Rails handler # because it's defined in the same recipe @handler = Kicker.process_chain[Kicker.process_chain.index(Kicker::Recipes::Rails) + 1] end it "should prepare the test database if db/schema.rb is modified" do Kicker::Utils.expects(:execute).with('rake db:test:prepare') @handler.call(%w{ db/schema.rb }) end it "should not prepare the test database if another file than db/schema.rb is modified" do Kicker::Utils.expects(:execute).never @handler.call(%w{ Rakefile }) end end module SharedRailsHandlerHelper def should_match(files, tests, existing_files=nil) File.use_original_exist = false File.existing_files = existing_files || tests @files += files Kicker::Recipes::Rails.call(@files) @files.should == %w{ Rakefile } end end describe "An instance of the Rails handler, with test type `test'" do extend SharedRailsHandlerHelper before do Kicker::Recipes::Ruby.reset! @files = %w{ Rakefile } end after do File.use_original_exist = true end it "should map model files to test/unit" do should_match %w{ app/models/member.rb app/models/article.rb }, %w{ test/unit/member_test.rb test/unit/article_test.rb } end it "should map concern files to test/unit/concerns" do should_match %w{ app/concerns/authenticate.rb app/concerns/nested_resource.rb }, %w{ test/unit/concerns/authenticate_test.rb test/unit/concerns/nested_resource_test.rb } end it "should map helper files to test/unit/helpers" do should_match %w{ app/helpers/members_helper.rb app/helpers/articles_helper.rb }, %w{ test/unit/helpers/members_helper_test.rb test/unit/helpers/articles_helper_test.rb } end it "should map controller files to test/functional" do should_match %w{ app/controllers/application_controller.rb app/controllers/members_controller.rb }, %w{ test/functional/application_controller_test.rb test/functional/members_controller_test.rb } end it "should map view templates to test/functional" do should_match %w{ app/views/members/index.html.erb app/views/admin/articles/show.html.erb }, %w{ test/functional/members_controller_test.rb test/functional/admin/articles_controller_test.rb } end it "should run all functional tests when config/routes.rb is saved" do tests = %w{ test/functional/members_controller_test.rb test/functional/admin/articles_controller_test.rb } Kicker::Recipes::Rails.expects(:all_controller_tests).returns(tests) should_match %w{ config/routes.rb }, tests end it "should map lib files to test/lib" do should_match %w{ lib/money.rb lib/views/date.rb }, %w{ test/lib/money_test.rb test/lib/views/date_test.rb } end it "should map fixtures to their unit, helper and functional tests if they exist" do tests = %w{ test/unit/member_test.rb test/unit/helpers/members_helper_test.rb test/functional/members_controller_test.rb } should_match %w{ test/fixtures/members.yml }, tests, [] Kicker::Recipes::Rails.tests_ran.last.should == [] end it "should map fixtures to their unit, helper and functional tests if they exist" do tests = %w{ test/unit/member_test.rb test/unit/helpers/members_helper_test.rb test/functional/members_controller_test.rb } should_match %w{ test/fixtures/members.yml }, tests Kicker::Recipes::Rails.tests_ran.last.should == tests end end describe "An instance of the Rails handler, with test type `spec'" do extend SharedRailsHandlerHelper before do Kicker::Recipes::Ruby.reset! Kicker::Recipes::Ruby.test_type = 'spec' @files = %w{ Rakefile } end after do File.use_original_exist = true end it "should map model files to spec/models" do should_match %w{ app/models/member.rb app/models/article.rb }, %w{ spec/models/member_spec.rb spec/models/article_spec.rb } end it "should map concern files to spec/models/concerns" do should_match %w{ app/concerns/authenticate.rb app/concerns/nested_resource.rb }, %w{ spec/models/concerns/authenticate_spec.rb spec/models/concerns/nested_resource_spec.rb } end it "should map helper files to spec/helpers" do should_match %w{ app/helpers/members_helper.rb app/helpers/articles_helper.rb }, %w{ spec/helpers/members_helper_spec.rb spec/helpers/articles_helper_spec.rb } end it "should map controller files to spec/controllers" do should_match %w{ app/controllers/application_controller.rb app/controllers/members_controller.rb }, %w{ spec/controllers/application_controller_spec.rb spec/controllers/members_controller_spec.rb } end it "should map view templates to spec/controllers" do should_match %w{ app/views/members/index.html.erb app/views/admin/articles/show.html.erb }, %w{ spec/controllers/members_controller_spec.rb spec/controllers/admin/articles_controller_spec.rb } end it "should run all controller tests when config/routes.rb is saved" do specs = %w{ spec/controllers/members_controller_test.rb spec/controllers/admin/articles_controller_test.rb } Kicker::Recipes::Rails.expects(:all_controller_tests).returns(specs) should_match %w{ config/routes.rb }, specs end it "should map lib files to spec/lib" do should_match %w{ lib/money.rb lib/views/date.rb }, %w{ spec/lib/money_spec.rb spec/lib/views/date_spec.rb } end it "should map fixtures to their model, helper and controller specs" do specs = %w{ spec/models/member_spec.rb spec/helpers/members_helper_spec.rb spec/controllers/members_controller_spec.rb } should_match %w{ spec/fixtures/members.yml }, specs end it "should map fixtures to their model, helper and controller specs if they exist" do specs = %w{ spec/models/member_spec.rb spec/helpers/members_helper_spec.rb spec/controllers/members_controller_spec.rb } should_match %w{ spec/fixtures/members.yml }, specs end end ================================================ FILE: spec/recipes/ruby_spec.rb ================================================ require File.expand_path('../../spec_helper', __FILE__) recipe :ruby class Kicker::Recipes::Ruby class << self attr_accessor :executed attr_accessor :blocks def execute(command, &block) self.executed ||= [] self.blocks ||= [] self.executed << command self.blocks << block end end end describe "The Ruby handler" do before do @handler = Kicker::Recipes::Ruby @handler.reset! @handler.test_type = 'test' end after do File.use_original_exist = true end it "should instantiate a handler instance when called" do tests = %w{ test/1_test.rb Rakefile test/namespace/2_test.rb } instance = @handler.new(tests) @handler.expects(:new).with(tests).returns(instance) @handler.call(tests) end it "should discover whether to use `ruby' or `spec' as the test_type" do File.use_original_exist = false File.existing_files = [] @handler.test_type.should == 'test' @handler.reset! File.existing_files = ['spec'] @handler.test_type.should == 'spec' end it "should run the given tests with a test-unit runner" do @handler.run_tests(%w{ test/1_test.rb test/namespace/2_test.rb }) @handler.executed.last.should == "ruby -I. -r test/1_test -r test/namespace/2_test -e ''" end it "should run the given tests with a spec runner" do @handler.test_type = 'spec' @handler.run_tests(%w{ test/1_test.rb test/namespace/2_test.rb }) @handler.executed.last.should == "rspec test/1_test.rb test/namespace/2_test.rb" end it "should not try to run the tests if none were given" do @handler.executed = [] @handler.run_tests([]) @handler.executed.should.be.empty end it "should be possible to override the bin path" do @handler.runner_bin = '/some/other/runner' @handler.run_tests(%w{ test/1_test.rb test/namespace/2_test.rb }) @handler.executed.last.should == "/some/other/runner -I. -r test/1_test -r test/namespace/2_test -e ''" end it "should set the alternative ruby bin path" do Kicker::Options.parse(%w{ -b /opt/ruby-1.9.2/bin/ruby }) @handler.runner_bin.should == '/opt/ruby-1.9.2/bin/ruby' @handler.reset! Kicker::Options.parse(%w{ --ruby /opt/ruby-1.9.2/bin/ruby }) @handler.runner_bin.should == '/opt/ruby-1.9.2/bin/ruby' end it "should be possible to add runner options when test_type is `test'" do @handler.test_type = 'test' @handler.test_options << '-I ./other' @handler.run_tests(%w{ test/1_test.rb }) @handler.executed.last.should == "ruby -I. -I ./other -r test/1_test -e ''" end it "should be possible to add runner options when test_type is `spec'" do @handler.test_type = 'spec' @handler.test_options << '-I ./other' @handler.run_tests(%w{ spec/1_spec.rb }) @handler.executed.last.should == "rspec -I ./other spec/1_spec.rb" end end %w{ test spec }.each do |type| describe "An instance of the Ruby handler, with test type `#{type}'" do before do @handler = Kicker::Recipes::Ruby @test_type, @test_cases_root = @handler.test_type, @handler.test_cases_root @handler.test_type = type @handler.test_cases_root = type File.use_original_exist = false File.existing_files = %W(#{type}/1_#{type}.rb #{type}/namespace/2_#{type}.rb) end after do @handler.test_type, @handler.test_cases_root = @test_type, @test_cases_root File.use_original_exist = true end it "should match any test case files" do files = %w(Rakefile) + File.existing_files handler = @handler.new(files) handler.handle! handler.tests.should == File.existing_files files.should == %W{ Rakefile } end it "should match files in ./lib" do files = %w(Rakefile) + File.existing_files handler = @handler.new(files) handler.handle! handler.tests.should == File.existing_files files.should == %w{ Rakefile } end it "should match lib tests in the test root as well" do File.existing_files = %W(#{type}/1_#{type}.rb #{type}/2_#{type}.rb) files = %W{ Rakefile lib/1.rb lib/namespace/2.rb } handler = @handler.new(files) handler.handle! handler.tests.should == %W{ #{type}/1_#{type}.rb #{type}/2_#{type}.rb } files.should == %W{ Rakefile } end it "should check if a different test case root" do @handler.test_cases_root = 'test/cases' files = %W{ Rakefile test/cases/1_#{type}.rb test/cases/namespace/2_#{type}.rb } handler = @handler.new(files) handler.handle! handler.tests.should == %W{ test/cases/1_#{type}.rb test/cases/namespace/2_#{type}.rb } files.should == %W{ Rakefile } end end end describe "The Ruby Bundler handler" do before do # We assume the Ruby Bundler handler is in the chain after the Ruby handler # because it's defined in the same recipe @handler = Kicker.process_chain[Kicker.process_chain.index(Kicker::Recipes::Ruby) + 1] end it "runs `bundle install` whenever the Gemfile changes" do Kicker::Utils.expects(:execute).with('bundle install') @handler.call(%w{ Gemfile }) end it "does not run `bundle install` when another file is changed" do Kicker::Utils.expects(:execute).never @handler.call(%w{ Rakefile }) end end ================================================ FILE: spec/recipes_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) require 'fakefs/safe' module ReloadDotKick; end describe "Kicker::Recipes" do RECIPES_PATH = Pathname.new('../../lib/kicker/recipes/').expand_path(__FILE__) def recipe_files Kicker::Recipes.recipe_files end before do Kicker::Recipes.reset! end before do FakeFS.activate! FakeFS::FileSystem.clone(RECIPES_PATH) Dir.chdir(File.absolute_path('../../', __FILE__)) end after do FakeFS.deactivate! FakeFS::FileSystem.clear end it "returns a list of recipes" do Pathname.glob(RECIPES_PATH.join('**/*.rb')) do |path| recipe_files.should.include?(path.expand_path) end end it "loads local recipes" do local = Pathname.new('~/.kick').expand_path local.mkpath recipe = local.join('some-random-recipe.rb') FileUtils.touch(recipe) recipe_files.should.include?(recipe.expand_path) end it "loads recipes in current working dir" do pwd = Pathname.pwd.join('.kick') pwd.mkpath recipe = pwd.expand_path.join('cwd-recipe.rb') FileUtils.touch(recipe) recipe_files.should.include(recipe.expand_path) end it "returns a list of recipe names" do expected = Set.new(%w(could_not_handle_file dot_kick execute_cli_command ignore jstest rails ruby).map { |n| n.to_sym }) actual = Set.new(Kicker::Recipes.recipe_names) actual.should == expected end # TODO ~/.kick is no longer added to the load path, but files are looked up # in lib/kicker/recipes.rb recipe_filename # #if File.exist?(File.expand_path('~/.kick')) #it "should add ~/.kick to the load path" do #$:.should.include File.expand_path('~/.kick') #end #else #puts "[!] ~/.kick does not exist, not testing the Kicker directory support." #end it "should load a recipe" do should.not.raise { recipe :ruby } end it "does not break when a recipe is loaded twice" do should.not.raise do recipe :ruby recipe :ruby end end it "should define a recipe load callback" do called = false recipe('new_recipe') { called = true } called.should == false recipe(:new_recipe) called.should == true end it "should raise if a recipe does not exist" do begin recipe :foobar rescue LoadError => e e.message.should.start_with "Can't load recipe `foobar', it doesn't exist on disk." end end end ================================================ FILE: spec/spec_helper.rb ================================================ require 'bacon' require 'mocha-on-bacon' Bacon.summary_at_exit require 'set' $:.unshift File.expand_path('../../lib', __FILE__) require 'kicker' class File class << self attr_accessor :existing_files attr_accessor :use_original_exist alias exist_without_stubbing? exist? def exist?(file) if use_original_exist exist_without_stubbing?(file) else if existing_files existing_files.include?(file) else raise "Please stub the files you want to exist by setting File.existing_files" end end end end end File.use_original_exist = true ================================================ FILE: spec/utils_spec.rb ================================================ require File.expand_path('../spec_helper', __FILE__) class Kicker module Utils public :will_execute_command, :did_execute_command end end describe "A Kicker instance, concerning its utility methods" do def utils Kicker::Utils end before do utils.stubs(:puts) Kicker::Notification.use = false Kicker::Notification.stubs(:`) end after do Kicker.silent = false Kicker::Notification.use = true end it "should print a log entry with timestamp" do now = Time.now Time.stubs(:now).returns(now) utils.expects(:puts).with("#{now.strftime('%H:%M:%S')}.#{now.usec.to_s[0,2]} | the message") utils.send(:log, 'the message') end it 'should print a log entry with no timestamp in quiet mode' do before = Kicker.quiet utils.expects(:puts).with('the message') Kicker.quiet = true utils.send(:log, 'the message') Kicker.quiet = before end it "logs that the command succeeded" do utils.stubs(:_execute).with do |job| job.output = "line 1\nline 2" job.exit_code = 0 end utils.expects(:log).with('Executing: ls') utils.expects(:log).with('Success') utils.execute('ls') end it "logs that the command failed" do utils.stubs(:_execute).with do |job| job.output = "line 1\nline 2" job.exit_code = 123 end utils.expects(:log).with('Executing: ls') utils.expects(:log).with('Failed (123)') utils.execute('ls') end it "calls the block given to execute and yields the job so the user can transform the output" do Kicker.silent = true utils.stubs(:_execute).with do |job| job.output = "line 1\nline 2" job.exit_code = 123 end utils.expects(:log).with('Executing: ls -l') utils.expects(:puts).with("\nOhnoes!\n\n") utils.expects(:log).with('Failed (123)') utils.execute('ls -l') do |job| job.output = job.success? ? 'Done!' : 'Ohnoes!' end end before do Kicker::Notification.use = true end it "notifies that a change occurred and shows the command and then the output" do utils.stubs(:log) utils.stubs(:_execute).with do |job| job.output = "line 1\nline 2" end Kicker::Notification.expects(:notify).with(:title => 'Kicker: Executing', :message => "ls") Kicker::Notification.expects(:notify).with(:title => 'Kicker: Success', :message => "line 1\nline 2") utils.execute('ls') end it "does not notify that a change occured in silent mode" do Kicker.silent = true utils.stubs(:did_execute_command) utils.expects(:log) Kicker::Notification.expects(:change_occured).never utils.execute('ls') end it "only logs that it has succeeded in silent mode" do Kicker.silent = true Kicker::Notification.expects(:notify).with(:title => "Kicker: Success", :message => "") job = Kicker::Job.new(:command => 'ls -l', :exit_code => 0, :output => "line 1\nline 2") utils.expects(:log).with("Success") utils.did_execute_command(job) end it "fully logs that it has failed in silent mode" do Kicker.silent = true Kicker::Notification.expects(:notify).with(:title => "Kicker: Failed (123)", :message => "") utils.expects(:puts).with("\nline 1\nline 2\n\n") utils.expects(:log).with('Failed (123)') job = Kicker::Job.new(:command => 'ls -l', :exit_code => 123, :output => "line 1\nline 2") utils.did_execute_command(job) end end describe "Kernel utility methods" do def utils Kicker::Utils end it "should forward log calls to the Kicker::Utils module" do utils.expects(:log).with('the message') log 'the message' end it "should forward execute calls to the Kicker::Utils module" do utils.expects(:execute).with('ls') execute 'ls' end end