├── .gitignore ├── History.txt ├── LICENSE ├── README.md ├── Rakefile ├── TODO.md ├── bin └── watchr ├── contributions.txt ├── docs.watchr ├── gem.watchr ├── lib ├── watchr.rb └── watchr │ ├── controller.rb │ ├── event_handlers │ ├── base.rb │ ├── darwin.rb │ ├── portable.rb │ └── unix.rb │ └── script.rb ├── specs.watchr ├── test ├── README ├── event_handlers │ ├── test_base.rb │ ├── test_darwin.rb │ ├── test_portable.rb │ └── test_unix.rb ├── test_controller.rb ├── test_helper.rb ├── test_script.rb └── test_watchr.rb └── watchr.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | pkg/ 3 | bk/ 4 | .wiki 5 | .yardoc 6 | *.gem 7 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | 2 | === v0.5.7 3 | 4 | * Added manifest.watchr script 5 | * Unix handler supports :deleted event type 6 | * Unix handler supports :accessed (atime), :modified (mtime) and :changed 7 | (ctime) event types (thanks gzuki[http://github.com/gzuki] for initial work) 8 | 9 | 10 | === v0.5.6 11 | 12 | * Rev gem optional in development (thanks TwP[http://github.com/TwP]) 13 | * Allow gems to bundle .watchr scripts (thanks foca[http://github.com/foca]) 14 | 15 | gemname/lib/gemname.watchr 16 | 17 | is now automatically picked up with 18 | 19 | $ watchr gemname.watchr 20 | 21 | * Look for script in path 22 | * debug msg when rev not found on *nix 23 | * rake task for cross interpreter testing 24 | 25 | 26 | === v0.5.5 27 | 28 | * Rev gem is optional. Fixes issue #1 29 | Install Rev to automatically get evented handler on *nix 30 | 31 | gem install rev 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2009 Martin Aumont (mynyml) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Summary 2 | ------- 3 | 4 | Agile development tool that monitors a directory tree, and triggers a user 5 | defined action whenever an observed file is modified. Its most typical use is 6 | continuous testing, and as such it is a more flexible alternative to autotest. 7 | 8 | Features 9 | -------- 10 | 11 | watchr is: 12 | 13 | * Simple to use 14 | * Highly flexible 15 | * Evented ( Listens for filesystem events with native c libs ) 16 | * Portable ( Linux, \*BSD, OSX, Solaris, Windows ) 17 | * Fast ( Immediately reacts to file changes ) 18 | 19 | Most importantly it allows running tests in an environment that is **agnostic** to: 20 | 21 | * Web frameworks ( rails, merb, sinatra, camping, invisible, ... ) 22 | * Test frameworks ( test/unit, minitest, rspec, test/spec, expectations, ... ) 23 | * Ruby interpreters ( ruby1.8, ruby1.9, MRI, JRuby, Rubinius, ... ) 24 | * Package frameworks ( rubygems, rip, ... ) 25 | 26 | Usage 27 | ----- 28 | 29 | On the command line, 30 | 31 | $ watchr path/to/script.file 32 | 33 | will monitor files in the current directory tree, and react to events on those 34 | files in accordance with the script. 35 | 36 | Scripts 37 | ------- 38 | 39 | The script contains a set of simple rules that map observed files to an action. 40 | Its DSL is a single method: `watch(pattern, &action)` 41 | 42 | watch( 'a regexp pattern matching paths to observe' ) {|match_data_object| command_to_run } 43 | 44 | So for example, 45 | 46 | watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") } 47 | 48 | will match any test file and run it whenever it is saved. 49 | 50 | A continuous testing script for a basic project could be 51 | 52 | watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") } 53 | watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") } 54 | 55 | which, in addition to running any saved test file as above, will also run a 56 | lib file's associated test. This mimics the equivalent autotest behaviour. 57 | 58 | It's easy to see why watchr is so flexible, since the whole command is custom. 59 | The above actions could just as easily call "jruby", "ruby --rubygems", "ruby 60 | -Ilib", "specrb", "rbx", ... or any combination of these. For the sake of 61 | comparison, autotest runs with: 62 | 63 | $ /usr/bin/ruby1.8 -I.:lib:test -rubygems -e "%w[test/unit test/test_helper.rb test/test_watchr.rb].each { |f| require f }" 64 | 65 | locking the environment into ruby1.8, rubygems and test/unit for all tests. 66 | 67 | And remember the scripts are pure ruby, so feel free to add methods, 68 | `Signal#trap` calls, etc. Updates to script files are picked up on the fly (no 69 | need to restart watchr) so experimenting is painless. 70 | 71 | The [wiki][5] has more details and examples. You might also want to take a 72 | look at watchr's own scripts, [specs.watchr][1], [docs.watchr][2] and 73 | [gem.watchr][3], to get you started. 74 | 75 | Install 76 | ------- 77 | 78 | gem install watchr 79 | 80 | If you're on Linux/BSD and have the [rev][4] gem installed, Watchr will detect 81 | it and use it automatically. This will make Watchr evented. 82 | 83 | gem install rev 84 | 85 | You can get the same evented behaviour on OS X by installing 86 | [ruby-fsevent][10]. 87 | 88 | gem install ruby-fsevent 89 | 90 | See Also 91 | -------- 92 | 93 | * [redgreen][6]: Standalone redgreen eye candy for test results, ala autotest. 94 | * [phocus][7]: Run focused tests when running the whole file/suite is unnecessary. 95 | * [autowatchr][8]: Provides some autotest-like behavior for watchr 96 | * [nestor][9]: Continuous testing server for Rails 97 | 98 | Links 99 | ----- 100 | 101 | * code: 102 | * docs: 103 | * wiki: 104 | * bugs: 105 | 106 | 107 | 108 | 109 | [1]: http://github.com/mynyml/watchr/blob/master/specs.watchr 110 | [2]: http://github.com/mynyml/watchr/blob/master/docs.watchr 111 | [3]: http://github.com/mynyml/watchr/blob/master/gem.watchr 112 | [4]: http://github.com/tarcieri/rev/ 113 | [5]: http://wiki.github.com/mynyml/watchr 114 | [6]: http://github.com/mynyml/redgreen 115 | [7]: http://github.com/mynyml/phocus 116 | [8]: http://github.com/viking/autowatchr 117 | [9]: http://github.com/francois/nestor 118 | [10]: http://github.com/sandro/ruby-fsevent 119 | 120 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | def gem_opt 2 | defined?(Gem) ? "-rubygems" : "" 3 | end 4 | 5 | def ruby 6 | require 'rbconfig' 7 | File.join([Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']]) << Config::CONFIG['EXEEXT'] 8 | end 9 | 10 | # -------------------------------------------------- 11 | # Tests 12 | # -------------------------------------------------- 13 | task(:default => "test:all") 14 | 15 | namespace(:test) do 16 | 17 | desc "Run all tests" 18 | task(:all) do 19 | tests = Dir['test/**/test_*.rb'] - ['test/test_helper.rb'] 20 | exit system(%Q{#{ruby} #{gem_opt} -I.:lib -e"%w( #{tests.join(' ')} ).each {|file| require file }"}) 21 | end 22 | 23 | desc "Run all tests on multiple ruby versions (requires rvm)" 24 | task(:portability) do 25 | %w( 1.8.6 1.8.7 1.9.1 1.9.2 ).each do |version| 26 | system <<-BASH 27 | bash -c 'source ~/.rvm/scripts/rvm; 28 | rvm #{version}; 29 | echo "--------- v#{version} ----------\n"; 30 | rake -s test:all' 31 | BASH 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Features 2 | -------- 3 | 4 | * watchr -e ( `$ watchr -e "watch('foo.gemspec') { system('gem build foo.gemspec') }"` ) 5 | * watchr --auto 6 | * watchr --fetch 7 | 8 | * enable ability to watch dirs 9 | * requires new handler(s) 10 | * will allow recognizing `:added` events 11 | 12 | * allow setting latency 13 | 14 | Bugs 15 | ---- 16 | 17 | * sometimes an action is fired without a file being saved 18 | * buffer flushing issue? 19 | * libev issue? 20 | * probably fixed with event type handling update, which ignores atime 21 | updates by defaults 22 | 23 | * when a file is saved twice quickly, subsequent events are ignored. 24 | * seems like rev/libev drops the file watch 25 | 26 | Other 27 | ----- 28 | 29 | * add tests for executable 30 | * memory profiling / benchmarks 31 | 32 | -------------------------------------------------------------------------------- /bin/watchr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pathname' 4 | require 'optparse' 5 | require 'tempfile' 6 | 7 | require File.dirname(__FILE__) + '/../lib/watchr' 8 | 9 | module Watchr 10 | # Namespaced to avoid defining global methods 11 | # 12 | # @private 13 | module Bin 14 | extend self 15 | 16 | DEFAULT_SCRIPT_PATH = Pathname.new('specs.watchr') 17 | 18 | attr_accessor :path 19 | 20 | def usage 21 | "Usage: watchr [opts] path/to/script" 22 | end 23 | 24 | def version 25 | "watchr version: %s" % Watchr::VERSION 26 | end 27 | 28 | # Absolute path to script file 29 | # 30 | # Unless set manually, the script's path is either first arg or 31 | # `DEFAULT_SCRIPT_PATH`. If neither exists, the script immediatly aborts 32 | # with an appropriate error message. 33 | # 34 | # @return [Pathname] 35 | # absolute path to script 36 | # 37 | def path! 38 | return @path unless @path.nil? 39 | rel = relative_path or abort( usage ) 40 | find_in_load_path(rel) or abort("no script found: file #{rel.to_s.inspect} is not in path.") 41 | end 42 | 43 | # Find a partial path name in load path 44 | # 45 | # @param [Pathname] path 46 | # partial pathname 47 | # 48 | # @return [Pathname] 49 | # absolute path of first occurence of partial path in load path, or nil if not found 50 | # 51 | def find_in_load_path(path) 52 | # Adds '.' for ruby1.9.2 53 | dir = (['.'] + $LOAD_PATH).uniq.detect {|p| Pathname(p).join(path).exist? } 54 | dir ? path.expand_path(dir) : nil 55 | end 56 | 57 | private 58 | 59 | def relative_path 60 | return Pathname.new(ARGV.first) if ARGV.first 61 | return DEFAULT_SCRIPT_PATH if DEFAULT_SCRIPT_PATH.exist? 62 | end 63 | end 64 | end 65 | 66 | opts = OptionParser.new do |opts| 67 | opts.banner = Watchr::Bin.usage 68 | 69 | opts.on('-d', '--debug', "Print extra debug info while program runs") { 70 | Watchr.options.debug = true 71 | begin 72 | require 'ruby-debug' 73 | rescue LoadError, RuntimeError 74 | end 75 | } 76 | opts.on('-l', '--list', "Display list of files monitored by script and exit") { 77 | script = Watchr::Script.new(Watchr::Bin.path!) 78 | controller = Watchr::Controller.new(script, Watchr.handler.new) 79 | script.parse! 80 | puts controller.monitored_paths 81 | exit 82 | } 83 | 84 | def assert_syntax(code) 85 | catch(:ok) { Object.new.instance_eval("BEGIN { throw :ok }; #{code}", %|-e "#{code}"|, 0) } 86 | rescue SyntaxError => e 87 | puts e.message.split("\n")[1] 88 | exit 89 | end 90 | 91 | opts.on('-e', '--eval INLINE_SCRIPT', %|Evaluate script inline ($ watchr -e "watch('foo') { puts 'bar' }")|) {|code| 92 | assert_syntax(code) 93 | 94 | Tempfile.open('foo') {|f| f << code; @__path = f.path } 95 | Watchr::Bin.path = Pathname(@__path) 96 | } 97 | 98 | opts.on_tail('-h', '--help', "Print inline help") { puts opts; exit } 99 | opts.on_tail('-v', '--version', "Print version" ) { puts Watchr::Bin.version; exit } 100 | 101 | opts.parse! ARGV 102 | end 103 | 104 | Watchr::Controller.new(Watchr::Script.new(Watchr::Bin.path!), Watchr.handler.new).run 105 | 106 | -------------------------------------------------------------------------------- /contributions.txt: -------------------------------------------------------------------------------- 1 | 2 | Suggestions: 3 | macournoyer, foca 4 | 5 | Patches: 6 | TwP, gzuki, spraints, francois 7 | 8 | -------------------------------------------------------------------------------- /docs.watchr: -------------------------------------------------------------------------------- 1 | # Run me with: 2 | # $ watchr docs.watchr 3 | 4 | require 'yard' 5 | # -------------------------------------------------- 6 | # Rules 7 | # -------------------------------------------------- 8 | watch( 'lib/.*\.rb' ) { yard } 9 | watch( 'README.md' ) { yard } 10 | watch( 'TODO.md' ) { yard } 11 | watch( 'LICENSE' ) { yard } 12 | 13 | # -------------------------------------------------- 14 | # Signal Handling 15 | # -------------------------------------------------- 16 | Signal.trap('QUIT') { yard } # Ctrl-\ 17 | Signal.trap('INT' ) { abort("\n") } # Ctrl-C 18 | 19 | # -------------------------------------------------- 20 | # Helpers 21 | # -------------------------------------------------- 22 | def yard 23 | print "Updating yardocs... "; STDOUT.flush 24 | YARD::CLI::Yardoc.run *%w( -o doc/yard --readme README.md --markup markdown - LICENSE TODO.md ) 25 | print "done\n" 26 | end 27 | -------------------------------------------------------------------------------- /gem.watchr: -------------------------------------------------------------------------------- 1 | # Run me with: 2 | # $ watchr gem.watchr 3 | 4 | def gemspec() Dir['*.gemspec'].first end 5 | # -------------------------------------------------- 6 | # Rules 7 | # -------------------------------------------------- 8 | watch( gemspec ) { build } 9 | 10 | # -------------------------------------------------- 11 | # Signal Handling 12 | # -------------------------------------------------- 13 | Signal.trap('QUIT') { build } # Ctrl-\ 14 | Signal.trap('INT' ) { abort("\n") } # Ctrl-C 15 | 16 | # -------------------------------------------------- 17 | # Helpers 18 | # -------------------------------------------------- 19 | def build 20 | puts; system "gem build #{gemspec}" 21 | FileUtils.mv( Dir['*.gem'], 'pkg/' ) 22 | end 23 | -------------------------------------------------------------------------------- /lib/watchr.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'rbconfig' 3 | 4 | # Agile development tool that monitors a directory recursively, and triggers a 5 | # user defined action whenever an observed file is modified. Its most typical 6 | # use is continuous testing. 7 | # 8 | # See README for more details 9 | # 10 | # @example 11 | # 12 | # # on command line, from project's root dir 13 | # $ watchr path/to/script 14 | # 15 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 16 | module Watchr 17 | VERSION = '0.7' 18 | 19 | begin 20 | require 'fsevent' 21 | HAVE_FSE = true 22 | rescue LoadError, RuntimeError 23 | HAVE_FSE = false 24 | end 25 | 26 | begin 27 | require 'rev' 28 | HAVE_REV = true 29 | rescue LoadError, RuntimeError 30 | HAVE_REV = false 31 | end 32 | 33 | autoload :Script, 'watchr/script' 34 | autoload :Controller, 'watchr/controller' 35 | 36 | module EventHandler 37 | autoload :Base, 'watchr/event_handlers/base' 38 | autoload :Portable, 'watchr/event_handlers/portable' 39 | autoload :Unix, 'watchr/event_handlers/unix' if ::Watchr::HAVE_REV 40 | autoload :Darwin, 'watchr/event_handlers/darwin' if ::Watchr::HAVE_FSE 41 | end 42 | 43 | class << self 44 | attr_accessor :options 45 | attr_accessor :handler 46 | 47 | # @deprecated 48 | def version #:nodoc: 49 | Watchr::VERSION 50 | end 51 | 52 | # Options proxy. 53 | # 54 | # Currently supported options: 55 | # 56 | # * debug[Boolean] Debugging state. More verbose. 57 | # 58 | # @example 59 | # 60 | # Watchr.options.debug #=> false 61 | # Watchr.options.debug = true 62 | # 63 | # @return [Struct] 64 | # options proxy. 65 | # 66 | def options 67 | @options ||= Struct.new(:debug).new 68 | @options.debug ||= false 69 | @options 70 | end 71 | 72 | # Outputs formatted debug statement to stdout, only if `::options.debug` is true 73 | # 74 | # @example 75 | # 76 | # Watchr.options.debug = true 77 | # Watchr.debug('im in ur codes, notifayinin u') 78 | # 79 | # #outputs: "[watchr debug] im in ur codes, notifayinin u" 80 | # 81 | # @param [String] message 82 | # debug message to print 83 | # 84 | # @return [nil] 85 | # 86 | def debug(msg) 87 | puts "[watchr debug] #{msg}" if options.debug 88 | end 89 | 90 | # Detect current OS and return appropriate handler. 91 | # 92 | # @example 93 | # 94 | # Config::CONFIG['host_os'] #=> 'linux-gnu' 95 | # Watchr.handler #=> Watchr::EventHandler::Unix 96 | # 97 | # Config::CONFIG['host_os'] #=> 'cygwin' 98 | # Watchr.handler #=> Watchr::EventHandler::Portable 99 | # 100 | # ENV['HANDLER'] #=> 'unix' 101 | # Watchr.handler #=> Watchr::EventHandler::Unix 102 | # 103 | # ENV['HANDLER'] #=> 'portable' 104 | # Watchr.handler #=> Watchr::EventHandler::Portable 105 | # 106 | # @return [Class] 107 | # handler class for current architecture 108 | # 109 | def handler 110 | @handler ||= 111 | case ENV['HANDLER'] || Config::CONFIG['host_os'] 112 | when /darwin|mach|osx|fsevents?/i 113 | if Watchr::HAVE_FSE 114 | Watchr::EventHandler::Darwin 115 | else 116 | Watchr.debug "fsevent not found. `gem install ruby-fsevent` to get evented handler" 117 | Watchr::EventHandler::Portable 118 | end 119 | when /sunos|solaris|bsd|linux|unix/i 120 | if Watchr::HAVE_REV 121 | Watchr::EventHandler::Unix 122 | else 123 | Watchr.debug "rev not found. `gem install rev` to get evented handler" 124 | Watchr::EventHandler::Portable 125 | end 126 | when /mswin|windows|cygwin/i 127 | Watchr::EventHandler::Portable 128 | else 129 | Watchr::EventHandler::Portable 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/watchr/controller.rb: -------------------------------------------------------------------------------- 1 | module Watchr 2 | 3 | # The controller contains the app's core logic. 4 | # 5 | # @example 6 | # 7 | # script = Watchr::Script.new(file) 8 | # contrl = Watchr::Controller.new(script, Watchr.handler.new) 9 | # contrl.run 10 | # 11 | # # Calling `run` will enter the listening loop, and from then on every 12 | # # file event will trigger its corresponding action defined in `script` 13 | # 14 | # # The controller also automatically adds the script's file to its list of 15 | # # monitored files and will detect any changes to it, providing on the fly 16 | # # updates of defined rules. 17 | # 18 | class Controller 19 | 20 | # Create a controller object around given `script` 21 | # 22 | # @param [Script] script 23 | # The script object 24 | # 25 | # @param [EventHandler::Base] handler 26 | # The filesystem event handler 27 | # 28 | # @see Watchr::Script 29 | # @see Watchr.handler 30 | # 31 | def initialize(script, handler) 32 | @script, @handler = script, handler 33 | @handler.add_observer(self) 34 | 35 | Watchr.debug "using %s handler" % handler.class.name 36 | end 37 | 38 | # Enter listening loop. Will block control flow until application is 39 | # explicitly stopped/killed. 40 | def run 41 | @script.parse! 42 | @handler.listen(monitored_paths) 43 | rescue Interrupt 44 | end 45 | 46 | # Callback for file events 47 | # 48 | # Called while control flow is in listening loop. It will execute the 49 | # file's corresponding action as defined in the script. If the file is the 50 | # script itself, it will refresh its state to account for potential changes. 51 | # 52 | # @param [Pathname, String] path 53 | # path that triggered the event 54 | # 55 | # @param [Symbol] event 56 | # event type 57 | # 58 | def update(path, event_type = nil) 59 | path = Pathname(path).expand_path 60 | 61 | Watchr.debug("received #{event_type.inspect} event for #{path.relative_path_from(Pathname(Dir.pwd))}") 62 | if path == @script.path && event_type != :accessed 63 | @script.parse! 64 | @handler.refresh(monitored_paths) 65 | else 66 | @script.action_for(path, event_type).call 67 | end 68 | end 69 | 70 | # List of paths the script is monitoring. 71 | # 72 | # Basically this means all paths below current directoly recursivelly that 73 | # match any of the rules' patterns, plus the script file. 74 | # 75 | # @return [Array] 76 | # list of all monitored paths 77 | # 78 | def monitored_paths 79 | paths = Dir['**/*'].select do |path| 80 | @script.patterns.any? {|p| path.match(p) } 81 | end 82 | paths.push(@script.path).compact! 83 | paths.map {|path| Pathname(path).expand_path } 84 | end 85 | end 86 | end 87 | 88 | -------------------------------------------------------------------------------- /lib/watchr/event_handlers/base.rb: -------------------------------------------------------------------------------- 1 | require 'observer' 2 | 3 | module Watchr 4 | module EventHandler 5 | 6 | # @private 7 | class AbstractMethod < Exception; end 8 | 9 | # Base functionality mixin, meant to be included in specific event handlers. 10 | # 11 | # @abstract 12 | module Base 13 | include Observable 14 | 15 | # Notify that a file was modified. 16 | # 17 | # @param [Pathname, String] path 18 | # full path or path relative to current working directory 19 | # 20 | # @param [Symbol] event 21 | # event type. 22 | # 23 | # @return [undefined] 24 | # 25 | def notify(path, event_type = nil) 26 | changed(true) 27 | notify_observers(path, event_type) 28 | end 29 | 30 | # Begin watching given paths and enter listening loop. Called by the 31 | # controller. 32 | # 33 | # @param [Array] monitored_paths 34 | # list of paths the application is currently monitoring. 35 | # 36 | # @return [undefined] 37 | # 38 | # @abstract 39 | def listen(monitored_paths) 40 | raise AbstractMethod 41 | end 42 | 43 | # Called by the controller when the list of paths monitored by wantchr 44 | # has changed. It should refresh the list of paths being watched. 45 | # 46 | # @param [Array] monitored_paths 47 | # list of paths the application is currently monitoring. 48 | # 49 | # @return [undefined] 50 | # 51 | # @abstract 52 | def refresh(monitored_paths) 53 | raise AbstractMethod 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/watchr/event_handlers/darwin.rb: -------------------------------------------------------------------------------- 1 | module Watchr 2 | module EventHandler 3 | 4 | class ::FSEvents 5 | # Same as Watch.debug, but prefixed with [fsevents] instead. 6 | # 7 | # @example 8 | # 9 | # FSEvents.debug('missfired') 10 | # 11 | # @param [String] message 12 | # debug message to print 13 | # 14 | # @return [nil] 15 | # 16 | def self.debug(msg) 17 | puts "[fsevents] #{msg}" if Watchr.options.debug 18 | end 19 | end 20 | 21 | # FSEvents based event handler for Darwin/OSX 22 | # 23 | # Uses ruby-fsevents (http://github.com/sandro/ruby-fsevent) 24 | # 25 | class Darwin < FSEvent 26 | include Base 27 | 28 | def initialize 29 | super 30 | self.latency = 0.2 31 | end 32 | 33 | # Enter listening loop. Will block control flow until application is 34 | # explicitly stopped/killed. 35 | # 36 | # @return [undefined] 37 | # 38 | def listen(monitored_paths) 39 | register_paths(monitored_paths) 40 | start 41 | end 42 | 43 | # Rebuild file bindings. Will detach all current bindings, and reattach 44 | # the `monitored_paths` 45 | # 46 | # @param [Array] monitored_paths 47 | # list of paths the application is currently monitoring. 48 | # 49 | # @return [undefined] 50 | # 51 | def refresh(monitored_paths) 52 | register_paths(monitored_paths) 53 | restart 54 | end 55 | 56 | private 57 | 58 | # Callback. Called on file change event. Delegates to 59 | # {Controller#update}, passing in path and event type 60 | # 61 | # @return [undefined] 62 | # 63 | def on_change(dirs) 64 | dirs.each do |dir| 65 | path, type = detect_change(dir) 66 | notify(path, type) unless path.nil? 67 | end 68 | end 69 | 70 | # Detected latest updated file within given directory 71 | # 72 | # @param [Pathname, String] dir 73 | # directory reporting event 74 | # 75 | # @return [Array(Pathname, Symbol)] path and type 76 | # path to updated file and event type 77 | # 78 | def detect_change(dir) 79 | paths = monitored_paths_for(dir) 80 | type = nil 81 | path = paths.find {|path| type = event_type(path) } 82 | 83 | FSEvents.debug("event detection error") if type.nil? 84 | 85 | update_reference_times 86 | [path, type] 87 | end 88 | 89 | # Detect type of event for path, if any 90 | # 91 | # Path times (atime, mtime, ctime) are compared to stored references. 92 | # If any is more recent, the event is reported as a symbol. 93 | # 94 | # @param [Pathname] path 95 | # 96 | # @return [Symbol, nil] event type 97 | # Event type if detected, nil otherwise. 98 | # Symbol is on of :deleted, :modified, :accessed, :changed 99 | # 100 | def event_type(path) 101 | return :deleted if !path.exist? 102 | return :modified if path.mtime > @reference_times[path][:mtime] 103 | return :accessed if path.atime > @reference_times[path][:atime] 104 | return :changed if path.ctime > @reference_times[path][:ctime] 105 | nil 106 | end 107 | 108 | # Monitored paths within given dir 109 | # 110 | # @param [Pathname, String] dir 111 | # 112 | # @return [Array] monitored_paths 113 | # 114 | def monitored_paths_for(dir) 115 | dir = Pathname(dir).expand_path 116 | @paths.select {|path| path.dirname.expand_path == dir } 117 | end 118 | 119 | # Register watches for paths 120 | # 121 | # @param [Array] paths 122 | # 123 | # @return [undefined] 124 | # 125 | def register_paths(paths) 126 | @paths = paths 127 | watch_directories(dirs_for(@paths)) 128 | update_reference_times 129 | end 130 | 131 | # Directories for paths 132 | # 133 | # A unique list of directories containing given paths 134 | # 135 | # @param [Array] paths 136 | # 137 | # @return [Array] dirs 138 | # 139 | def dirs_for(paths) 140 | paths.map {|path| path.dirname.to_s }.uniq 141 | end 142 | 143 | # Update reference times for registered paths 144 | # 145 | # @return [undefined] 146 | # 147 | def update_reference_times 148 | @reference_times = {} 149 | now = Time.now 150 | @paths.each do |path| 151 | @reference_times[path] = {} 152 | @reference_times[path][:atime] = now 153 | @reference_times[path][:mtime] = now 154 | @reference_times[path][:ctime] = now 155 | end 156 | end 157 | end 158 | end 159 | end 160 | 161 | -------------------------------------------------------------------------------- /lib/watchr/event_handlers/portable.rb: -------------------------------------------------------------------------------- 1 | module Watchr 2 | module EventHandler 3 | class Portable 4 | include Base 5 | 6 | def initialize 7 | @reference_mtime = @reference_atime = @reference_ctime = Time.now 8 | end 9 | 10 | # Enters listening loop. 11 | # 12 | # Will block control flow until application is explicitly stopped/killed. 13 | # 14 | # @param [Array] monitored_paths 15 | # list of paths the application is currently monitoring. 16 | # 17 | # @return [undefined] 18 | # 19 | def listen(monitored_paths) 20 | @monitored_paths = monitored_paths 21 | loop { trigger; sleep(1) } 22 | end 23 | 24 | # See if an event occured, and if so notify observers. 25 | # 26 | # @return [undefined] 27 | # 28 | # @private 29 | def trigger 30 | path, type = detect_event 31 | notify(path, type) unless path.nil? 32 | end 33 | 34 | # Update list of monitored paths. 35 | # 36 | # @param [Array] monitored_paths 37 | # list of paths the application is currently monitoring. 38 | # 39 | # @return [undefined] 40 | # 41 | def refresh(monitored_paths) 42 | @monitored_paths = monitored_paths 43 | end 44 | 45 | private 46 | 47 | # Verify mtimes of monitored files. 48 | # 49 | # If the latest mtime is more recent than the reference mtime, return 50 | # that file's path. 51 | # 52 | # @return [[Pathname, Symbol]] 53 | # path and type of event if event occured, nil otherwise 54 | # 55 | # @todo improve ENOENT error handling 56 | # 57 | def detect_event # OPTIMIZE, REFACTOR 58 | @monitored_paths.each do |path| 59 | return [path, :deleted] unless path.exist? 60 | end 61 | 62 | mtime_path = @monitored_paths.max {|a,b| a.mtime <=> b.mtime } 63 | atime_path = @monitored_paths.max {|a,b| a.atime <=> b.atime } 64 | ctime_path = @monitored_paths.max {|a,b| a.ctime <=> b.ctime } 65 | 66 | if mtime_path.mtime > @reference_mtime then @reference_mtime = mtime_path.mtime; [mtime_path, :modified] 67 | elsif atime_path.atime > @reference_atime then @reference_atime = atime_path.atime; [atime_path, :accessed] 68 | elsif ctime_path.ctime > @reference_ctime then @reference_ctime = ctime_path.ctime; [ctime_path, :changed ] 69 | else; nil; end 70 | rescue Errno::ENOENT 71 | retry 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/watchr/event_handlers/unix.rb: -------------------------------------------------------------------------------- 1 | module Watchr 2 | module EventHandler 3 | class Unix 4 | include Base 5 | 6 | # Used by Rev. Wraps a monitored path, and `Rev::Loop` will call its 7 | # callback on file events. 8 | # 9 | # @private 10 | class SingleFileWatcher < Rev::StatWatcher 11 | class << self 12 | # Stores a reference back to handler so we can call its {Base#notify notify} 13 | # method with file event info 14 | # 15 | # @return [EventHandler::Base] 16 | # 17 | attr_accessor :handler 18 | end 19 | 20 | # @param [String] path 21 | # single file to monitor 22 | # 23 | def initialize(path) 24 | super 25 | update_reference_times 26 | end 27 | 28 | # File's path as a Pathname 29 | # 30 | # @return [Pathname] 31 | # 32 | def pathname 33 | @pathname ||= Pathname(@path) 34 | end 35 | 36 | # Callback. Called on file change event. Delegates to 37 | # {Controller#update}, passing in path and event type 38 | # 39 | # @return [undefined] 40 | # 41 | def on_change 42 | self.class.handler.notify(path, type) 43 | update_reference_times unless type == :deleted 44 | end 45 | 46 | private 47 | 48 | # @todo improve ENOENT error handling 49 | def update_reference_times 50 | @reference_atime = pathname.atime 51 | @reference_mtime = pathname.mtime 52 | @reference_ctime = pathname.ctime 53 | rescue Errno::ENOENT 54 | retry 55 | end 56 | 57 | # Type of latest event. 58 | # 59 | # A single type is determined, even though more than one stat times may 60 | # have changed on the file. The type is the first to match in the 61 | # following hierarchy: 62 | # 63 | # :deleted, :modified (mtime), :accessed (atime), :changed (ctime) 64 | # 65 | # @return [Symbol] type 66 | # latest event's type 67 | # 68 | def type 69 | return :deleted if !pathname.exist? 70 | return :modified if pathname.mtime > @reference_mtime 71 | return :accessed if pathname.atime > @reference_atime 72 | return :changed if pathname.ctime > @reference_ctime 73 | end 74 | end 75 | 76 | def initialize 77 | SingleFileWatcher.handler = self 78 | @loop = Rev::Loop.default 79 | end 80 | 81 | # Enters listening loop. Will block control flow until application is 82 | # explicitly stopped/killed. 83 | # 84 | # @return [undefined] 85 | # 86 | def listen(monitored_paths) 87 | @monitored_paths = monitored_paths 88 | attach 89 | @loop.run 90 | end 91 | 92 | # Rebuilds file bindings. Will detach all current bindings, and reattach 93 | # the `monitored_paths` 94 | # 95 | # @param [Array] monitored_paths 96 | # list of paths the application is currently monitoring. 97 | # 98 | # @return [undefined] 99 | # 100 | def refresh(monitored_paths) 101 | @monitored_paths = monitored_paths 102 | detach 103 | attach 104 | end 105 | 106 | private 107 | 108 | # Binds all `monitored_paths` to the listening loop. 109 | # 110 | # @return [undefined] 111 | # 112 | def attach 113 | @monitored_paths.each {|path| SingleFileWatcher.new(path.to_s).attach(@loop) } 114 | end 115 | 116 | # Unbinds all paths currently attached to listening loop. 117 | # 118 | # @return [undefined] 119 | # 120 | def detach 121 | @loop.watchers.each {|watcher| watcher.detach } 122 | end 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/watchr/script.rb: -------------------------------------------------------------------------------- 1 | module Watchr 2 | 3 | # A script object wraps a script file, and is used by a controller. 4 | # 5 | # @example 6 | # 7 | # path = Pathname.new('specs.watchr') 8 | # script = Watchr::Script.new(path) 9 | # 10 | class Script 11 | 12 | # @private 13 | DEFAULT_EVENT_TYPE = :modified 14 | 15 | # Convenience type. Provides clearer and simpler access to rule properties. 16 | # 17 | # @example 18 | # 19 | # rule = script.watch('lib/.*\.rb') { 'ohaie' } 20 | # rule.pattern #=> 'lib/.*\.rb' 21 | # rule.action.call #=> 'ohaie' 22 | # 23 | Rule = Struct.new(:pattern, :event_type, :action) 24 | 25 | # Script file evaluation context 26 | # 27 | # Script files are evaluated in the context of an instance of this class so 28 | # that they get a clearly defined set of methods to work with. In other 29 | # words, it is the user script's API. 30 | # 31 | # @private 32 | class EvalContext #:nodoc: 33 | 34 | def initialize(script) 35 | @__script = script 36 | end 37 | 38 | # Delegated to script 39 | def default_action(&action) 40 | @__script.default_action(&action) 41 | end 42 | 43 | # Delegated to script 44 | def watch(*args, &block) 45 | @__script.watch(*args, &block) 46 | end 47 | 48 | # Reload script 49 | def reload 50 | @__script.parse! 51 | end 52 | end 53 | 54 | # EvalContext instance 55 | # 56 | # @example 57 | # 58 | # script.ec.watch('pattern') { } 59 | # script.ec.reload 60 | # 61 | # @return [EvalContext] 62 | # 63 | attr_reader :ec 64 | 65 | # Defined rules 66 | # 67 | # @return [Rule] 68 | # all rules defined with `#watch` calls 69 | # 70 | attr_reader :rules 71 | 72 | # Create a Script object for script at `path` 73 | # 74 | # @param [Pathname] path 75 | # the path to the script 76 | # 77 | def initialize(path = nil) 78 | @path = path 79 | @rules = [] 80 | @default_action = Proc.new {} 81 | @ec = EvalContext.new(self) 82 | end 83 | 84 | # Main script API method. Builds a new rule, binding a pattern to an action. 85 | # 86 | # Whenever a file is saved that matches a rule's `pattern`, its 87 | # corresponding `action` is triggered. 88 | # 89 | # Patterns can be either a Regexp or a string. Because they always 90 | # represent paths however, it's simpler to use strings. But remember to use 91 | # single quotes (not double quotes), otherwise escape sequences will be 92 | # parsed (for example `"foo/bar\.rb" #=> "foo/bar.rb"`, notice "\." becomes 93 | # "."), and won't be interpreted as the regexp you expect. 94 | # 95 | # Also note that patterns will be matched against relative paths (relative 96 | # to current working directory). 97 | # 98 | # Actions, the blocks passed to `watch`, receive a `MatchData` object as 99 | # argument. It will be populated with the whole matched string ( `md[0]` ) 100 | # as well as individual backreferences ( `md[1..n]` ). See `MatchData#[]` 101 | # documentation for more details. 102 | # 103 | # @example 104 | # 105 | # # in script file 106 | # watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") } 107 | # watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") } 108 | # 109 | # With these two rules, watchr will run any test file whenever it is itself 110 | # changed (first rule), and will also run a corresponding test file 111 | # whenever a lib file is changed (second rule). 112 | # 113 | # @param [#match] pattern 114 | # pattern to match targetted paths 115 | # 116 | # @param [Symbol] event_type 117 | # rule will only match events of this type. Accepted types are 118 | # `:accessed`, `:modified`, `:changed`, `:delete` and `nil` (any), where 119 | # the first three correspond to atime, mtime and ctime respectively. 120 | # Defaults to `:modified`. 121 | # 122 | # @yield 123 | # action to trigger 124 | # 125 | # @return [Rule] 126 | # 127 | def watch(pattern, event_type = DEFAULT_EVENT_TYPE, &action) 128 | @rules << Rule.new(pattern, event_type, action || @default_action) 129 | @rules.last 130 | end 131 | 132 | # Convenience method. Define a default action to be triggered when a rule 133 | # has none specified. When called without a block, acts as a getter and 134 | # returns stored default_action 135 | # 136 | # @example 137 | # 138 | # # in script file 139 | # 140 | # default_action { system('rake --silent yard') } 141 | # 142 | # watch( 'lib/.*\.rb' ) 143 | # watch( 'README.md' ) 144 | # watch( 'TODO.txt' ) 145 | # watch( 'LICENSE' ) 146 | # 147 | # # is equivalent to: 148 | # 149 | # watch( 'lib/.*\.rb' ) { system('rake --silent yard') } 150 | # watch( 'README.md' ) { system('rake --silent yard') } 151 | # watch( 'TODO.txt' ) { system('rake --silent yard') } 152 | # watch( 'LICENSE' ) { system('rake --silent yard') } 153 | # 154 | # @return [Proc] 155 | # default action 156 | # 157 | def default_action(&action) 158 | @default_action = action if action 159 | @default_action 160 | end 161 | 162 | # Reset script state 163 | def reset 164 | @rules = [] 165 | @default_action = Proc.new {} 166 | end 167 | 168 | # Eval content of script file. 169 | # 170 | # @todo improve ENOENT error handling 171 | def parse! 172 | return unless @path 173 | reset 174 | @ec.instance_eval(@path.read, @path.to_s) 175 | rescue Errno::ENOENT 176 | sleep(0.3) #enough? 177 | retry 178 | ensure 179 | Watchr.debug('loaded script file %s' % @path.to_s.inspect) 180 | end 181 | 182 | # Find an action corresponding to a path and event type. The returned 183 | # action is actually a wrapper around the rule's action, with the 184 | # match_data prepopulated. 185 | # 186 | # @example 187 | # 188 | # script.watch( 'test/test_.*\.rb' ) {|md| "ruby #{md[0]}" } 189 | # script.action_for('test/test_watchr.rb').call #=> "ruby test/test_watchr.rb" 190 | # 191 | # @param [Pathname, String] path 192 | # find action that corresponds to this path. 193 | # 194 | # @param [Symbol] event_type 195 | # find action only if rule's event is of this type. 196 | # 197 | # @return [Proc] 198 | # action, preparsed and ready to be called 199 | # 200 | def action_for(path, event_type = DEFAULT_EVENT_TYPE) 201 | path = rel_path(path).to_s 202 | rule = rules_for(path).detect {|rule| rule.event_type.nil? || rule.event_type == event_type } 203 | if rule 204 | data = path.match(rule.pattern) 205 | lambda { rule.action.call(data) } 206 | else 207 | lambda {} 208 | end 209 | end 210 | 211 | # Collection of all patterns defined in script. 212 | # 213 | # @return [Array] 214 | # all defined patterns 215 | # 216 | def patterns 217 | #@rules.every.pattern 218 | @rules.map {|r| r.pattern } 219 | end 220 | 221 | # Path to the script file corresponding to this object 222 | # 223 | # @return [Pathname] 224 | # absolute path to script file 225 | # 226 | def path 227 | @path && Pathname(@path.respond_to?(:to_path) ? @path.to_path : @path.to_s).expand_path 228 | end 229 | 230 | private 231 | 232 | # Rules corresponding to a given path, in reversed order of precedence 233 | # (latest one is most inportant). 234 | # 235 | # @param [Pathname, String] path 236 | # path to look up rule for 237 | # 238 | # @return [Array] 239 | # rules corresponding to `path` 240 | # 241 | def rules_for(path) 242 | @rules.reverse.select {|rule| path.match(rule.pattern) } 243 | end 244 | 245 | # Make a path relative to current working directory. 246 | # 247 | # @param [Pathname, String] path 248 | # absolute or relative path 249 | # 250 | # @return [Pathname] 251 | # relative path, from current working directory. 252 | # 253 | def rel_path(path) 254 | Pathname(path).expand_path.relative_path_from(Pathname(Dir.pwd)) 255 | end 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /specs.watchr: -------------------------------------------------------------------------------- 1 | # Run me with: 2 | # $ watchr specs.watchr 3 | 4 | # -------------------------------------------------- 5 | # Rules 6 | # -------------------------------------------------- 7 | watch( '^test.*/test_.*\.rb' ) { |m| ruby m[0] } 8 | watch( '^lib/(.*)\.rb' ) { |m| ruby "test/test_#{m[1]}.rb" } 9 | watch( '^lib/watchr/(.*)\.rb' ) { |m| ruby "test/test_#{m[1]}.rb" } 10 | watch( '^lib/watchr/event_handlers/(.*)\.rb' ) { |m| ruby "test/event_handlers/test_#{m[1]}.rb" } 11 | watch( '^test/test_helper\.rb' ) { ruby tests } 12 | 13 | # -------------------------------------------------- 14 | # Signal Handling 15 | # -------------------------------------------------- 16 | Signal.trap('QUIT') { ruby tests } # Ctrl-\ 17 | Signal.trap('INT' ) { abort("\n") } # Ctrl-C 18 | 19 | # -------------------------------------------------- 20 | # Helpers 21 | # -------------------------------------------------- 22 | def ruby(*paths) 23 | run "ruby #{gem_opt} -I.:lib:test -e'%w( #{paths.flatten.join(' ')} ).each {|p| require p }'" 24 | end 25 | 26 | def tests 27 | Dir['test/**/test_*.rb'] - ['test/test_helper.rb'] 28 | end 29 | 30 | def run( cmd ) 31 | puts cmd 32 | system cmd 33 | end 34 | 35 | def gem_opt 36 | defined?(Gem) ? "-rubygems" : "" 37 | end 38 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | To use local watchr executable for dev work: 3 | 4 | $ ruby -rubygems -Ilib ./bin/watchr -d specs.watchr 5 | 6 | To force a specific handler: 7 | 8 | $ HANDLER=protable watchr -d specs.watchr 9 | $ HANDLER=unix watchr -d specs.watchr 10 | 11 | (see Watchr.handler) 12 | -------------------------------------------------------------------------------- /test/event_handlers/test_base.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | 3 | class BaseEventHandlerTest < MiniTest::Unit::TestCase 4 | 5 | class Handler 6 | include Watchr::EventHandler::Base 7 | end 8 | 9 | def setup 10 | @handler = Handler.new 11 | end 12 | 13 | test "api" do 14 | assert_respond_to @handler, :notify 15 | assert_respond_to @handler, :listen 16 | assert_respond_to @handler, :refresh 17 | assert_includes @handler.class.ancestors, Observable 18 | end 19 | 20 | test "notifies observers" do 21 | @handler.expects(:notify_observers).with('foo/bar', nil) 22 | @handler.notify('foo/bar', nil) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/event_handlers/test_darwin.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | 3 | if Watchr::HAVE_FSE 4 | 5 | class Watchr::EventHandler::Darwin 6 | attr_accessor :paths 7 | 8 | def start() end #noop 9 | def restart() end #noop 10 | 11 | public :on_change, :registered_directories 12 | end 13 | 14 | class DarwinEventHandlerTest < MiniTest::Unit::TestCase 15 | include Watchr 16 | 17 | def tempfile(name) 18 | file = Tempfile.new(name, @root.to_s) 19 | Pathname(file.path) 20 | ensure 21 | file.close 22 | end 23 | 24 | def setup 25 | @root = Pathname(Dir.mktmpdir("WATCHR_SPECS-")) 26 | 27 | @now = Time.now 28 | @handler = EventHandler::Darwin.new 29 | 30 | @foo = tempfile('foo').expand_path 31 | @bar = tempfile('bar').expand_path 32 | end 33 | 34 | def teardown 35 | FileUtils.remove_entry_secure(@root.to_s) 36 | end 37 | 38 | test "listening triggers listening state" do 39 | @handler.expects(:start) 40 | @handler.listen([]) 41 | end 42 | 43 | test "listens for events on monitored files" do 44 | @handler.listen [ @foo, @bar ] 45 | assert_includes @handler.paths, @foo 46 | assert_includes @handler.paths, @bar 47 | end 48 | 49 | test "reattaches to new monitored files" do 50 | @baz = tempfile('baz').expand_path 51 | @bax = tempfile('bax').expand_path 52 | 53 | @handler.listen [ @foo, @bar ] 54 | assert_includes @handler.paths, @foo 55 | assert_includes @handler.paths, @bar 56 | 57 | @handler.refresh [ @baz, @bax ] 58 | assert_includes @handler.paths, @baz 59 | assert_includes @handler.paths, @bax 60 | refute_includes @handler.paths, @foo 61 | refute_includes @handler.paths, @bar 62 | end 63 | 64 | ## event types 65 | 66 | test "deleted file event" do 67 | @foo.stubs(:exist?).returns(false) 68 | 69 | @handler.listen [ @foo, @bar ] 70 | @handler.expects(:notify).with(@foo, :deleted) 71 | @handler.on_change [@root] 72 | end 73 | 74 | test "modified file event" do 75 | @foo.stubs(:mtime).returns(@now + 100) 76 | @handler.expects(:notify).with(@foo, :modified) 77 | 78 | @handler.listen [ @foo, @bar ] 79 | @handler.on_change [@root] 80 | end 81 | 82 | test "accessed file event" do 83 | @foo.stubs(:atime).returns(@now + 100) 84 | @handler.expects(:notify).with(@foo, :accessed) 85 | 86 | @handler.listen [ @foo, @bar ] 87 | @handler.on_change [@root] 88 | end 89 | 90 | test "changed file event" do 91 | @foo.stubs(:ctime).returns(@now + 100) 92 | @handler.expects(:notify).with(@foo, :changed) 93 | 94 | @handler.listen [ @foo, @bar ] 95 | @handler.on_change [@root] 96 | end 97 | 98 | ## internal 99 | 100 | test "registers directories" do 101 | @handler.listen [ @foo, @bar ] 102 | 103 | assert_equal @foo.dirname, @bar.dirname # make sure all tempfiles are in same dir 104 | assert_equal 1, @handler.registered_directories.size 105 | assert_includes @handler.registered_directories, @foo.dirname.to_s 106 | assert_includes @handler.registered_directories, @bar.dirname.to_s 107 | end 108 | end 109 | 110 | end # if Watchr::HAVE_FSE 111 | 112 | -------------------------------------------------------------------------------- /test/event_handlers/test_portable.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | 3 | class Watchr::EventHandler::Portable 4 | attr_accessor :monitored_paths 5 | end 6 | 7 | class PortableEventHandlerTest < MiniTest::Unit::TestCase 8 | include Watchr 9 | 10 | def setup 11 | @handler = EventHandler::Portable.new 12 | @handler.stubs(:loop) 13 | 14 | @foo = Pathname('foo').expand_path 15 | @bar = Pathname('bar').expand_path 16 | @baz = Pathname('baz').expand_path 17 | @bax = Pathname('bax').expand_path 18 | 19 | @now = Time.now 20 | [@foo, @bar, @baz, @bax].each do |path| 21 | path.stubs(:mtime ).returns(@now - 100) 22 | path.stubs(:atime ).returns(@now - 100) 23 | path.stubs(:ctime ).returns(@now - 100) 24 | path.stubs(:exist?).returns(true) 25 | end 26 | end 27 | 28 | test "triggers listening state" do 29 | @handler.expects(:loop) 30 | @handler.listen([]) 31 | end 32 | 33 | ## monitoring file events 34 | 35 | test "listens for events on monitored files" do 36 | @handler.listen [ @foo, @bar ] 37 | assert_includes @handler.monitored_paths, @foo 38 | assert_includes @handler.monitored_paths, @bar 39 | end 40 | 41 | test "doesn't trigger on start" do 42 | end 43 | 44 | ## event types 45 | 46 | test "deleted file event" do 47 | @foo.stubs(:exist?).returns(false) 48 | 49 | @handler.listen [ @foo, @bar ] 50 | @handler.expects(:notify).with(@foo, :deleted) 51 | @handler.trigger 52 | end 53 | 54 | test "modified file event" do 55 | @foo.stubs(:mtime).returns(@now + 100) 56 | 57 | @handler.listen [ @foo, @bar ] 58 | @handler.expects(:notify).with(@foo, :modified) 59 | @handler.trigger 60 | end 61 | 62 | test "accessed file event" do 63 | @foo.stubs(:atime).returns(@now + 100) 64 | 65 | @handler.listen [ @foo, @bar ] 66 | @handler.expects(:notify).with(@foo, :accessed) 67 | @handler.trigger 68 | end 69 | 70 | test "changed file event" do 71 | @foo.stubs(:ctime).returns(@now + 100) 72 | 73 | @handler.listen [ @foo, @bar ] 74 | @handler.expects(:notify).with(@foo, :changed) 75 | @handler.trigger 76 | end 77 | 78 | ## event type priorities 79 | 80 | test "mtime > atime" do 81 | @foo.stubs(:mtime).returns(@now + 100) 82 | @foo.stubs(:atime).returns(@now + 100) 83 | @foo.stubs(:ctime).returns(@now + 100) 84 | 85 | @handler.listen [ @foo, @bar ] 86 | @handler.expects(:notify).with(@foo, :modified) 87 | @handler.trigger 88 | end 89 | 90 | test "mtime > ctime" do 91 | @foo.stubs(:mtime).returns(@now + 100) 92 | @foo.stubs(:ctime).returns(@now + 100) 93 | 94 | @handler.listen [ @foo, @bar ] 95 | @handler.expects(:notify).with(@foo, :modified) 96 | @handler.trigger 97 | end 98 | 99 | test "atime > ctime" do 100 | @foo.stubs(:atime).returns(@now + 100) 101 | @foo.stubs(:ctime).returns(@now + 100) 102 | 103 | @handler.listen [ @foo, @bar ] 104 | @handler.expects(:notify).with(@foo, :accessed) 105 | @handler.trigger 106 | end 107 | 108 | test "deleted > mtime" do 109 | @foo.stubs(:exist?).returns(false) 110 | @foo.stubs(:mtime ).returns(@now + 100) 111 | 112 | @handler.listen [ @foo, @bar ] 113 | @handler.expects(:notify).with(@foo, :deleted) 114 | @handler.trigger 115 | end 116 | 117 | ## on the fly updates of monitored files list 118 | 119 | test "reattaches to new monitored files" do 120 | @handler.listen [ @foo, @bar ] 121 | assert_includes @handler.monitored_paths, @foo 122 | assert_includes @handler.monitored_paths, @bar 123 | 124 | @handler.refresh [ @baz, @bax ] 125 | assert_includes @handler.monitored_paths, @baz 126 | assert_includes @handler.monitored_paths, @bax 127 | refute_includes @handler.monitored_paths, @foo 128 | refute_includes @handler.monitored_paths, @bar 129 | end 130 | 131 | test "retries on ENOENT errors" do 132 | @oops = Pathname('oops').expand_path 133 | @oops.stubs(:exist?).returns(true) 134 | @oops.stubs(:mtime).raises(Errno::ENOENT). 135 | then.returns(Time.now + 100) 136 | 137 | @handler.listen [ @oops ] 138 | 139 | @handler.expects(:notify).with(@oops, :modified) 140 | @handler.trigger 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /test/event_handlers/test_unix.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | 3 | if Watchr::HAVE_REV 4 | 5 | class Watchr::EventHandler::Unix::SingleFileWatcher 6 | public :type 7 | end 8 | 9 | class UnixEventHandlerTest < MiniTest::Unit::TestCase 10 | include Watchr 11 | 12 | SingleFileWatcher = EventHandler::Unix::SingleFileWatcher 13 | 14 | def setup 15 | @now = Time.now 16 | pathname = Pathname.new('foo/bar') 17 | pathname.stubs(:atime ).returns(@now) 18 | pathname.stubs(:mtime ).returns(@now) 19 | pathname.stubs(:ctime ).returns(@now) 20 | pathname.stubs(:exist?).returns(true) 21 | SingleFileWatcher.any_instance.stubs(:pathname).returns(pathname) 22 | 23 | @loop = Rev::Loop.default 24 | @handler = EventHandler::Unix.new 25 | @watcher = SingleFileWatcher.new('foo/bar') 26 | @loop.stubs(:run) 27 | end 28 | 29 | def teardown 30 | SingleFileWatcher.handler = nil 31 | Rev::Loop.default.watchers.every.detach 32 | end 33 | 34 | test "triggers listening state" do 35 | @loop.expects(:run) 36 | @handler.listen([]) 37 | end 38 | 39 | ## SingleFileWatcher 40 | 41 | test "watcher pathname" do 42 | assert_instance_of Pathname, @watcher.pathname 43 | assert_equal @watcher.path, @watcher.pathname.to_s 44 | end 45 | 46 | test "stores reference times" do 47 | @watcher.pathname.stubs(:atime).returns(:time) 48 | @watcher.pathname.stubs(:mtime).returns(:time) 49 | @watcher.pathname.stubs(:ctime).returns(:time) 50 | 51 | @watcher.send(:update_reference_times) 52 | assert_equal :time, @watcher.instance_variable_get(:@reference_atime) 53 | assert_equal :time, @watcher.instance_variable_get(:@reference_mtime) 54 | assert_equal :time, @watcher.instance_variable_get(:@reference_ctime) 55 | end 56 | 57 | test "stores initial reference times" do 58 | SingleFileWatcher.any_instance.expects(:update_reference_times) 59 | SingleFileWatcher.new('foo') 60 | end 61 | 62 | test "updates reference times on change" do 63 | @watcher.expects(:update_reference_times) 64 | @watcher.on_change 65 | end 66 | 67 | test "detects event type" do 68 | trigger_event @watcher, @now, :atime 69 | assert_equal :accessed, @watcher.type 70 | 71 | trigger_event @watcher, @now, :mtime 72 | assert_equal :modified, @watcher.type 73 | 74 | trigger_event @watcher, @now, :ctime 75 | assert_equal :changed, @watcher.type 76 | 77 | trigger_event @watcher, @now, :atime, :mtime 78 | assert_equal :modified, @watcher.type 79 | 80 | trigger_event @watcher, @now, :mtime, :ctime 81 | assert_equal :modified, @watcher.type 82 | 83 | trigger_event @watcher, @now, :atime, :ctime 84 | assert_equal :accessed, @watcher.type 85 | 86 | trigger_event @watcher, @now, :atime, :mtime, :ctime 87 | assert_equal :modified, @watcher.type 88 | 89 | @watcher.pathname.stubs(:exist?).returns(false) 90 | assert_equal :deleted, @watcher.type 91 | end 92 | 93 | ## monitoring file events 94 | 95 | test "listens for events on monitored files" do 96 | @handler.listen %w( foo bar ) 97 | assert_equal 2, @loop.watchers.size 98 | assert_equal %w( foo bar ).to_set, @loop.watchers.every.path.to_set 99 | assert_equal [SingleFileWatcher], @loop.watchers.every.class.uniq 100 | end 101 | 102 | test "notifies observers on file event" do 103 | @watcher.stubs(:path).returns('foo') 104 | @handler.expects(:notify).with('foo', anything) 105 | @watcher.on_change 106 | end 107 | 108 | test "notifies observers of event type" do 109 | trigger_event @watcher, @now, :atime 110 | @handler.expects(:notify).with('foo/bar', :accessed) 111 | @watcher.on_change 112 | 113 | trigger_event @watcher, @now, :mtime 114 | @handler.expects(:notify).with('foo/bar', :modified) 115 | @watcher.on_change 116 | 117 | trigger_event @watcher, @now, :ctime 118 | @handler.expects(:notify).with('foo/bar', :changed) 119 | @watcher.on_change 120 | 121 | trigger_event @watcher, @now, :atime, :mtime, :ctime 122 | @handler.expects(:notify).with('foo/bar', :modified) 123 | @watcher.on_change 124 | 125 | @watcher.pathname.stubs(:exist?).returns(false) 126 | @handler.expects(:notify).with('foo/bar', :deleted) 127 | @watcher.on_change 128 | end 129 | 130 | ## on the fly updates of monitored files list 131 | 132 | test "reattaches to new monitored files" do 133 | @handler.listen %w( foo bar ) 134 | assert_equal 2, @loop.watchers.size 135 | assert_includes @loop.watchers.every.path, 'foo' 136 | assert_includes @loop.watchers.every.path, 'bar' 137 | 138 | @handler.refresh %w( baz bax ) 139 | assert_equal 2, @loop.watchers.size 140 | assert_includes @loop.watchers.every.path, 'baz' 141 | assert_includes @loop.watchers.every.path, 'bax' 142 | refute_includes @loop.watchers.every.path, 'foo' 143 | refute_includes @loop.watchers.every.path, 'bar' 144 | end 145 | 146 | private 147 | 148 | def trigger_event(watcher, now, *types) 149 | watcher.pathname.stubs(:atime).returns(now) 150 | watcher.pathname.stubs(:mtime).returns(now) 151 | watcher.pathname.stubs(:ctime).returns(now) 152 | watcher.instance_variable_set(:@reference_atime, now) 153 | watcher.instance_variable_set(:@reference_mtime, now) 154 | watcher.instance_variable_set(:@reference_ctime, now) 155 | 156 | types.each do |type| 157 | watcher.pathname.stubs(type).returns(now+10) 158 | end 159 | end 160 | end 161 | 162 | end # if Watchr::HAVE_REV 163 | -------------------------------------------------------------------------------- /test/test_controller.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | require 'observer' 3 | 4 | class MockHandler 5 | include Observable 6 | def listen(paths) end 7 | def refresh(paths) end 8 | end 9 | 10 | class TestController < MiniTest::Unit::TestCase 11 | include Watchr 12 | 13 | def to_p(str) 14 | Pathname(str).expand_path 15 | end 16 | 17 | def setup 18 | tmpfile = Tempfile.new('foo') 19 | @script = Script.new( Pathname.new( tmpfile.path ) ) 20 | @handler = MockHandler.new 21 | @controller = Controller.new(@script, @handler) 22 | end 23 | 24 | test "triggers listening state on run" do 25 | @controller.stubs(:monitored_paths).returns %w( foo bar ) 26 | @handler.expects(:listen).with %w( foo bar ) 27 | @controller.run 28 | end 29 | 30 | test "parses the script on #run" do 31 | @script.expects(:parse!) 32 | @controller.run 33 | end 34 | 35 | test "adds itself as handler observer" do 36 | assert_equal 1, @handler.count_observers 37 | @handler.delete_observer(@controller) 38 | assert_equal 0, @handler.count_observers 39 | end 40 | 41 | ## monitored paths list 42 | 43 | test "fetches monitored paths" do 44 | Dir.expects(:[]).at_least_once.with('**/*').returns(%w( 45 | a 46 | b/x.z 47 | b/c 48 | b/c/y.z 49 | )) 50 | @script.watch('.\.z') { :x } 51 | 52 | contrl = Controller.new(@script, MockHandler.new) 53 | assert_includes contrl.monitored_paths, to_p('b/x.z') 54 | assert_includes contrl.monitored_paths, to_p('b/c/y.z') 55 | end 56 | 57 | test "doesn't fetch unmonitored paths" do 58 | Dir.expects(:[]).at_least_once.with('**/*').returns(%w( 59 | a 60 | b/x.z 61 | b/c 62 | b/c/y.z 63 | )) 64 | @script.watch('.\.z') { :x } 65 | 66 | contrl = Controller.new(@script, MockHandler.new) 67 | refute_includes contrl.monitored_paths, to_p('a') 68 | refute_includes contrl.monitored_paths, to_p('b/c') 69 | refute_includes contrl.monitored_paths, to_p('p/q.z') 70 | end 71 | 72 | test "monitored paths include script" do 73 | Dir.expects(:[]).at_least_once.with('**/*').returns(%w( a )) 74 | Script.any_instance.stubs(:parse!) 75 | 76 | path = to_p('some/file') 77 | script = Script.new(path) 78 | contrl = Controller.new(script, MockHandler.new) 79 | assert_includes contrl.monitored_paths, path 80 | end 81 | 82 | ## on update 83 | 84 | test "calls action for path" do 85 | path = to_p('abc') 86 | @script.expects(:action_for).with(path, :modified).returns(lambda {}) 87 | 88 | @controller.update('abc', :modified) 89 | end 90 | 91 | test "parses script on script file update" do 92 | path = to_p('abc') 93 | @script.stubs(:path).returns(path) 94 | @script.expects(:parse!) 95 | 96 | @controller.update('abc') 97 | end 98 | 99 | test "refreshes handler on script file update" do 100 | path = to_p('abc') 101 | @script.stubs(:path).returns(path) 102 | @controller.stubs(:monitored_paths).returns %w( foo bar ) 103 | 104 | @handler.expects(:refresh).with %w( foo bar ) 105 | @controller.update(path) 106 | end 107 | 108 | test "exits gracefully when Interrupted" do 109 | @handler.stubs(:listen).raises(Interrupt) 110 | @controller.run 111 | end 112 | 113 | test "does not parse script on mere script file access" do 114 | path = to_p('abc') 115 | @script.stubs(:path).returns(path) 116 | @script.expects(:parse!).never 117 | 118 | @controller.update('abc', :accessed) 119 | end 120 | end 121 | 122 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'tmpdir' 3 | require 'tempfile' 4 | require 'fileutils' 5 | 6 | require 'minitest/autorun' 7 | require 'mocha' 8 | require 'every' 9 | begin 10 | require 'redgreen' #http://gemcutter.org/gems/mynyml-redgreen 11 | require 'phocus' 12 | require 'ruby-debug' 13 | rescue LoadError, RuntimeError 14 | end 15 | 16 | require 'watchr' 17 | 18 | class MiniTest::Unit::TestCase 19 | class << self 20 | def test(name, &block) 21 | define_method("test_#{name.gsub(/\s/,'_')}", &block) 22 | end 23 | alias :should :test 24 | 25 | # noop 26 | def xtest(*args) end 27 | end 28 | end 29 | 30 | unless Watchr::HAVE_REV 31 | puts "Skipping Unix handler tests. Install Rev (gem install rev) to properly test full suite" 32 | end 33 | 34 | unless Watchr::HAVE_FSE 35 | puts "Skipping Darwin handler tests. Install FSEvent (gem install ruby-fsevent) to properly test full suite (osx only)" 36 | end 37 | 38 | -------------------------------------------------------------------------------- /test/test_script.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | 3 | class TestScript < MiniTest::Unit::TestCase 4 | include Watchr 5 | 6 | def setup 7 | @script = Script.new 8 | end 9 | 10 | ## external api 11 | 12 | test "watch" do 13 | @script.ec.watch('pattern') 14 | @script.ec.watch('pattern', :event_type) 15 | @script.ec.watch('pattern') { nil } 16 | end 17 | 18 | test "default action" do 19 | @script.ec.default_action { nil } 20 | end 21 | 22 | test "reload" do 23 | @script.ec.reload 24 | end 25 | 26 | test "eval context delegates methods to script" do 27 | @script.ec.watch('pattern') 28 | @script.ec.watch('pattern', :event_type) 29 | @script.ec.watch('pattern') { nil } 30 | @script.ec.default_action { :foo } 31 | 32 | assert_equal 3, @script.rules.size 33 | assert_equal :foo, @script.default_action.call 34 | end 35 | 36 | ## functionality 37 | 38 | test "rule object" do 39 | rule = @script.watch('pattern', :modified) { nil } 40 | 41 | assert_equal 'pattern', rule.pattern 42 | assert_equal :modified, rule.event_type 43 | assert_equal nil, rule.action.call 44 | end 45 | 46 | test "default event type" do 47 | rule = @script.watch('pattern') { nil } 48 | assert_equal :modified, rule.event_type 49 | end 50 | 51 | test "finds action for path" do 52 | @script.watch('abc') { :x } 53 | @script.watch('def') { :y } 54 | assert_equal :x, @script.action_for('abc').call 55 | end 56 | 57 | test "finds action for path with event type" do 58 | @script.watch('abc', :accessed) { :x } 59 | @script.watch('abc', :modified) { :y } 60 | assert_equal :x, @script.action_for('abc', :accessed).call 61 | end 62 | 63 | test "finds action for path with any event type" do 64 | @script.watch('abc', nil) { :x } 65 | @script.watch('abc', :modified) { :y } 66 | assert_equal :x, @script.action_for('abc', :accessed).call 67 | end 68 | 69 | test "no action for path" do 70 | @script.watch('abc', :accessed) { :x } 71 | assert_nil @script.action_for('abc', :modified).call 72 | end 73 | 74 | test "collects patterns" do 75 | @script.watch('abc') 76 | @script.watch('def') 77 | assert_includes @script.patterns, 'abc' 78 | assert_includes @script.patterns, 'def' 79 | end 80 | 81 | test "parses script file" do 82 | path = Pathname( Tempfile.open('bar').path ) 83 | path.open('w') {|f| f.write <<-STR } 84 | watch( 'abc' ) { :x } 85 | STR 86 | script = Script.new(path) 87 | script.parse! 88 | assert_equal :x, script.action_for('abc').call 89 | end 90 | 91 | test "__FILE__ is set properly in script file" do 92 | path = Pathname( Tempfile.open('bar').path ) 93 | path.open('w') {|f| f.write <<-STR } 94 | throw __FILE__.to_sym 95 | STR 96 | script = Script.new(path) 97 | assert_throws(path.to_s.to_sym) { script.parse! } 98 | end 99 | 100 | test "reloads script file" do 101 | @script.expects(:parse!) 102 | @script.ec.reload 103 | end 104 | 105 | test "skips parsing on nil script file" do 106 | script = Script.new 107 | script.ec.stubs(:instance_eval).raises(Exception) #negative expectation hack 108 | script.parse! 109 | end 110 | 111 | test "resets state" do 112 | @script.default_action { 'x' } 113 | @script.watch('foo') { 'bar' } 114 | @script.reset 115 | assert_nil @script.default_action.call 116 | assert_equal [], @script.rules 117 | end 118 | 119 | test "resets state on parse" do 120 | script = Script.new( Pathname( Tempfile.new('foo').path ) ) 121 | script.stubs(:instance_eval) 122 | script.expects(:reset) 123 | script.parse! 124 | end 125 | 126 | test "actions receive a MatchData object" do 127 | @script.watch('de(.)') {|m| [m[0], m[1]] } 128 | assert_equal %w( def f ), @script.action_for('def').call 129 | end 130 | 131 | test "rule's default action" do 132 | @script.watch('abc') 133 | assert_nil @script.action_for('abc').call 134 | 135 | @script.default_action { :x } 136 | @script.watch('def') 137 | assert_equal :x, @script.action_for('def').call 138 | end 139 | 140 | test "file path" do 141 | Script.any_instance.stubs(:parse!) 142 | path = Pathname('some/file').expand_path 143 | script = Script.new(path) 144 | assert_equal path, script.path 145 | end 146 | 147 | test "nil file path" do 148 | script = Script.new 149 | assert_nil script.path 150 | end 151 | 152 | test "later rules take precedence" do 153 | @script.watch('a/(.*)\.x') { :x } 154 | @script.watch('a/b/(.*)\.x') { :y } 155 | assert_equal :y, @script.action_for('a/b/c.x').call 156 | end 157 | 158 | test "rule patterns match against paths relative to pwd" do 159 | @script.watch('^abc') { :x } 160 | path = Pathname(Dir.pwd) + 'abc' 161 | assert_equal :x, @script.action_for(path).call 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/test_watchr.rb: -------------------------------------------------------------------------------- 1 | require 'test/test_helper' 2 | 3 | class TestWatchr < MiniTest::Unit::TestCase 4 | 5 | def setup 6 | Watchr.options = nil 7 | end 8 | 9 | ## options 10 | 11 | test "debug option" do 12 | assert_equal false, Watchr.options.debug 13 | Watchr.options.debug = true 14 | assert_equal true, Watchr.options.debug 15 | end 16 | 17 | ## functionality 18 | 19 | test "debug" do 20 | assert_empty capture_io { Watchr.debug('abc') }.first 21 | Watchr.options.debug = true 22 | assert_equal "[watchr debug] abc\n", capture_io { Watchr.debug('abc') }.first 23 | end 24 | 25 | test "picking handler" do 26 | 27 | if Watchr::HAVE_REV 28 | 29 | Watchr.handler = nil 30 | ENV['HANDLER'] = 'linux' 31 | assert_equal Watchr::EventHandler::Unix, Watchr.handler 32 | 33 | Watchr.handler = nil 34 | ENV['HANDLER'] = 'bsd' 35 | assert_equal Watchr::EventHandler::Unix, Watchr.handler 36 | 37 | Watchr.handler = nil 38 | ENV['HANDLER'] = 'unix' 39 | assert_equal Watchr::EventHandler::Unix, Watchr.handler 40 | 41 | end 42 | 43 | if Watchr::HAVE_FSE 44 | 45 | Watchr.handler = nil 46 | ENV['HANDLER'] = 'darwin' 47 | assert_equal Watchr::EventHandler::Darwin, Watchr.handler 48 | 49 | Watchr.handler = nil 50 | ENV['HANDLER'] = 'osx' 51 | assert_equal Watchr::EventHandler::Darwin, Watchr.handler 52 | 53 | Watchr.handler = nil 54 | ENV['HANDLER'] = 'fsevent' 55 | assert_equal Watchr::EventHandler::Darwin, Watchr.handler 56 | 57 | end 58 | 59 | Watchr.handler = nil 60 | ENV['HANDLER'] = 'mswin' 61 | assert_equal Watchr::EventHandler::Portable, Watchr.handler 62 | 63 | Watchr.handler = nil 64 | ENV['HANDLER'] = 'cygwin' 65 | assert_equal Watchr::EventHandler::Portable, Watchr.handler 66 | 67 | Watchr.handler = nil 68 | ENV['HANDLER'] = 'portable' 69 | assert_equal Watchr::EventHandler::Portable, Watchr.handler 70 | 71 | Watchr.handler = nil 72 | ENV['HANDLER'] = 'other' 73 | assert_equal Watchr::EventHandler::Portable, Watchr.handler 74 | end 75 | end 76 | 77 | -------------------------------------------------------------------------------- /watchr.gemspec: -------------------------------------------------------------------------------- 1 | require 'lib/watchr' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "watchr" 5 | s.summary = "Modern continious testing (flexible alternative to autotest)" 6 | s.description = "Modern continious testing (flexible alternative to autotest)." 7 | s.author = "mynyml" 8 | s.email = "mynyml@gmail.com" 9 | s.homepage = "http://mynyml.com/ruby/flexible-continuous-testing" 10 | s.rubyforge_project = "watchr" 11 | s.require_path = "lib" 12 | s.bindir = "bin" 13 | s.executables = "watchr" 14 | s.version = Watchr::VERSION 15 | s.files = `git ls-files`.strip.split("\n") 16 | 17 | s.add_development_dependency 'minitest' 18 | s.add_development_dependency 'mocha' 19 | s.add_development_dependency 'every' 20 | end 21 | --------------------------------------------------------------------------------