tekkub-watchr 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +19 -0
- data/README.rdoc +94 -0
- data/Rakefile +85 -0
- data/TODO.txt +31 -0
- data/bin/watchr +39 -0
- data/docs.watchr +26 -0
- data/lib/watchr.rb +76 -0
- data/lib/watchr/controller.rb +79 -0
- data/lib/watchr/event_handlers/base.rb +48 -0
- data/lib/watchr/event_handlers/portable.rb +55 -0
- data/lib/watchr/event_handlers/unix.rb +62 -0
- data/lib/watchr/script.rb +192 -0
- data/lib/watchr/version.rb +11 -0
- data/specs.watchr +44 -0
- data/test/event_handlers/test_base.rb +24 -0
- data/test/event_handlers/test_portable.rb +58 -0
- data/test/event_handlers/test_unix.rb +56 -0
- data/test/test_controller.rb +104 -0
- data/test/test_helper.rb +50 -0
- data/test/test_script.rb +88 -0
- data/test/test_watchr.rb +59 -0
- data/watchr.gemspec +91 -0
- metadata +91 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
=== Summary
|
2
|
+
|
3
|
+
Agile development tool that monitors a directory tree, and triggers a user
|
4
|
+
defined action whenever an observed file is modified. Its most typical use is
|
5
|
+
continuous testing, and as such it is a more flexible alternative to autotest.
|
6
|
+
|
7
|
+
|
8
|
+
=== Features
|
9
|
+
|
10
|
+
watchr is:
|
11
|
+
|
12
|
+
* Simple to use
|
13
|
+
* Highly *flexible*
|
14
|
+
* Evented ( Listens for filesystem events with native c libs )
|
15
|
+
* Portable ( Linux, *BSD, OSX, Solaris, Windows )
|
16
|
+
* Fast ( Immediately reacts to file changes )
|
17
|
+
|
18
|
+
Most importantly it allows running tests in an environment that is *agnostic* to:
|
19
|
+
|
20
|
+
* Web frameworks ( rails, merb, sinatra, camping, invisible, ... )
|
21
|
+
* Test frameworks ( test/unit, minitest, rspec, test/spec, expectations, ... )
|
22
|
+
* Ruby interpreters ( ruby1.8, ruby1.9, MRI, JRuby, Rubinius, ... )
|
23
|
+
* Package frameworks ( rubygems, rip, ... )
|
24
|
+
|
25
|
+
|
26
|
+
=== Usage
|
27
|
+
|
28
|
+
On the command line,
|
29
|
+
|
30
|
+
$ watchr path/to/script.file
|
31
|
+
|
32
|
+
will monitor files in the current directory tree, and react to events on those
|
33
|
+
files in accordance with the script.
|
34
|
+
|
35
|
+
|
36
|
+
=== Scripts
|
37
|
+
|
38
|
+
The script contains a set of simple rules that map observed files to an action.
|
39
|
+
Its DSL is a single method: watch(pattern, &action)
|
40
|
+
|
41
|
+
watch( 'a regexp pattern matching paths to observe' ) {|match_data_object| command_to_run }
|
42
|
+
|
43
|
+
So for example,
|
44
|
+
|
45
|
+
watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
|
46
|
+
|
47
|
+
will match any test file and run it whenever it is saved.
|
48
|
+
|
49
|
+
A continuous testing script for a basic project could be
|
50
|
+
|
51
|
+
watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
|
52
|
+
watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") }
|
53
|
+
|
54
|
+
which, in addition to running any saved test file as above, will also run a
|
55
|
+
lib file's associated test. This mimics the equivalent autotest behaviour.
|
56
|
+
|
57
|
+
It's easy to see why watchr is so flexible, since the whole command is custom.
|
58
|
+
The above actions could just as easily call "jruby", "ruby --rubygems", "ruby
|
59
|
+
-Ilib", "specrb", "rbx", ..., or any combination of these. For the sake of
|
60
|
+
comparison, autotest runs with:
|
61
|
+
|
62
|
+
/usr/bin/ruby1.8 -I.:lib:test -rubygems -e "%w[test/unit test/test_helper.rb test/test_watchr.rb].each { |f| require f }"
|
63
|
+
|
64
|
+
locking the environment into ruby1.8, rubygems and test/unit for all tests.
|
65
|
+
|
66
|
+
And remember the scripts are pure ruby, so feel free to add methods,
|
67
|
+
Signal#trap calls, etc. Updates to script files are picked up on the fly (no
|
68
|
+
need to restart watchr) so experimenting is painless.
|
69
|
+
|
70
|
+
The wiki[http://wiki.github.com/mynyml/watchr] has more details and examples.
|
71
|
+
You can also take a look at watchr's own specs.watchr script it the root dir,
|
72
|
+
as well as docs.watchr
|
73
|
+
|
74
|
+
|
75
|
+
=== Install
|
76
|
+
|
77
|
+
gem install mynyml-watchr --source http://gems.github.com/
|
78
|
+
|
79
|
+
|
80
|
+
=== See Also
|
81
|
+
|
82
|
+
redgreen[http://github.com/mynyml/redgreen]:: Standalone redgreen eye candy for test results, ala autotest.
|
83
|
+
phocus[http://github.com/mynyml/phocus]:: Run focused tests when running the whole file/suite is unnecessary.
|
84
|
+
|
85
|
+
|
86
|
+
=== Links
|
87
|
+
|
88
|
+
source:: http://github.com/mynyml/watchr
|
89
|
+
rdocs:: http://docs.github.com/mynyml/watchr
|
90
|
+
wiki:: http://wiki.github.com/mynyml/watchr
|
91
|
+
|
92
|
+
=== Acknowledgement
|
93
|
+
|
94
|
+
* Thanks to macournoyer[http://github.com/macournoyer] for the evented backend idea
|
data/Rakefile
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# --------------------------------------------------
|
2
|
+
# based on thin's Rakefile (http://github.com/macournoyer/thin)
|
3
|
+
# --------------------------------------------------
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'pathname'
|
7
|
+
require 'yaml'
|
8
|
+
require 'lib/watchr/version'
|
9
|
+
begin
|
10
|
+
require 'yard'
|
11
|
+
rescue LoadError, RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
RUBY_1_9 = RUBY_VERSION =~ /^1\.9/
|
15
|
+
WIN = (RUBY_PLATFORM =~ /mswin|cygwin/)
|
16
|
+
SUDO = (WIN ? "" : "sudo")
|
17
|
+
|
18
|
+
def gem
|
19
|
+
RUBY_1_9 ? 'gem19' : 'gem'
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_except(res)
|
23
|
+
Dir['**/*'].reject do |path|
|
24
|
+
Array(res).any? {|re| path.match(re) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
spec = Gem::Specification.new do |s|
|
29
|
+
s.name = 'watchr'
|
30
|
+
s.version = Watchr.version
|
31
|
+
s.summary = "Continious anything"
|
32
|
+
s.description = "Continious anything; project files observer/trigger."
|
33
|
+
s.author = "Martin Aumont"
|
34
|
+
s.email = 'mynyml@gmail.com'
|
35
|
+
s.homepage = ''
|
36
|
+
s.has_rdoc = true
|
37
|
+
s.require_path = "lib"
|
38
|
+
s.bindir = "bin"
|
39
|
+
s.executables = "watchr"
|
40
|
+
s.files = all_except %w( ^doc/ ^doc$ ^pkg ^bk ^\.wiki ^\.yardoc )
|
41
|
+
#s.add_dependency 'every', '>= 1.0'
|
42
|
+
s.add_dependency 'rev', '>= 0.3.0'
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Generate rdoc documentation."
|
46
|
+
Rake::RDocTask.new(:rdoc => 'rdoc', :clobber_rdoc => 'rdoc:clean', :rerdoc => 'rdoc:force') { |rdoc|
|
47
|
+
rdoc.rdoc_dir = 'doc/rdoc'
|
48
|
+
rdoc.title = "Watchr"
|
49
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
50
|
+
rdoc.options << '--charset' << 'utf-8'
|
51
|
+
rdoc.main = 'README.rdoc'
|
52
|
+
rdoc.rdoc_files.include('README.rdoc')
|
53
|
+
rdoc.rdoc_files.include('TODO.txt')
|
54
|
+
rdoc.rdoc_files.include('LICENSE')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
}
|
57
|
+
|
58
|
+
if defined? YARD
|
59
|
+
YARD::Rake::YardocTask.new do |t|
|
60
|
+
t.files = %w( lib/**/*.rb )
|
61
|
+
t.options = %w( -o doc/yard --readme README.rdoc --files LICENSE,TODO.txt )
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Rake::GemPackageTask.new(spec) do |p|
|
66
|
+
p.gem_spec = spec
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "Remove package products"
|
70
|
+
task :clean => :clobber_package
|
71
|
+
|
72
|
+
desc "Update the gemspec for GitHub's gem server"
|
73
|
+
task :gemspec do
|
74
|
+
Pathname("#{spec.name}.gemspec").open('w') {|f| f << YAML.dump(spec) }
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Install gem"
|
78
|
+
task :install => [:clobber, :package] do
|
79
|
+
sh "#{SUDO} #{gem} install pkg/#{spec.full_name}.gem"
|
80
|
+
end
|
81
|
+
|
82
|
+
desc "Uninstall gem"
|
83
|
+
task :uninstall => :clean do
|
84
|
+
sh "#{SUDO} #{gem} uninstall -v #{spec.version} -x #{spec.name}"
|
85
|
+
end
|
data/TODO.txt
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
* rev dependency should be conditional on OS
|
3
|
+
|
4
|
+
* fix issue with Script#parse!
|
5
|
+
* only accept paths in initialize?
|
6
|
+
|
7
|
+
* sometimes an action is fired without a file being saved
|
8
|
+
* buffer flushing issue?
|
9
|
+
* libev issue?
|
10
|
+
|
11
|
+
* test on other platforms
|
12
|
+
* mswin
|
13
|
+
* bsd
|
14
|
+
* osx
|
15
|
+
* solaris
|
16
|
+
|
17
|
+
* write a few prepackaged scripts
|
18
|
+
* post on gists
|
19
|
+
* post links on wiki
|
20
|
+
* post main links in readme
|
21
|
+
|
22
|
+
* eval script within own context?
|
23
|
+
|
24
|
+
* respond to different file events?
|
25
|
+
* modified
|
26
|
+
* created
|
27
|
+
* deleted
|
28
|
+
* etc.
|
29
|
+
* watch(pattern, EVENT, &action)
|
30
|
+
|
31
|
+
* memory profiling / benchmarks
|
data/bin/watchr
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
require 'watchr'
|
7
|
+
require 'watchr/version'
|
8
|
+
|
9
|
+
def usage
|
10
|
+
"Usage: watchr [opts] path/to/script"
|
11
|
+
end
|
12
|
+
def version
|
13
|
+
"watchr version: %s" % Watchr.version
|
14
|
+
end
|
15
|
+
|
16
|
+
opts = OptionParser.new do |opts|
|
17
|
+
opts.banner = usage
|
18
|
+
|
19
|
+
opts.on('-d', '--debug', "Print extra debug info while program runs") {
|
20
|
+
Watchr.options.debug = true
|
21
|
+
begin
|
22
|
+
require 'ruby-debug'
|
23
|
+
rescue LoadError, RuntimeError
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
opts.on_tail('-h', '--help', "Print inline help") { puts opts; exit }
|
28
|
+
opts.on_tail('-v', '--version', "Print version" ) { puts version; exit }
|
29
|
+
|
30
|
+
opts.parse! ARGV
|
31
|
+
end
|
32
|
+
|
33
|
+
abort(usage) unless ARGV.first
|
34
|
+
|
35
|
+
file = Pathname(ARGV.first).expand_path
|
36
|
+
abort(%|no script found; file "#{file.to_s}" doesn't exist.|) unless file.exist?
|
37
|
+
|
38
|
+
Watchr::Controller.new(Watchr::Script.new(file), Watchr.handler.new).run
|
39
|
+
|
data/docs.watchr
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Run me with:
|
2
|
+
#
|
3
|
+
# $ watchr docs.watchr
|
4
|
+
|
5
|
+
def run_rdoc
|
6
|
+
system('rake --silent rdoc')
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_yard
|
10
|
+
print "\nUpdating yardocs... "
|
11
|
+
system('rake --silent yardoc')
|
12
|
+
print "done.\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
def document
|
16
|
+
run_rdoc
|
17
|
+
run_yard
|
18
|
+
end
|
19
|
+
|
20
|
+
watch( 'lib/.*\.rb' ) { document }
|
21
|
+
watch( 'README.rdoc' ) { document }
|
22
|
+
watch( 'TODO.txt' ) { document }
|
23
|
+
watch( 'LICENSE' ) { document }
|
24
|
+
|
25
|
+
|
26
|
+
# vim:ft=ruby
|
data/lib/watchr.rb
ADDED
@@ -0,0 +1,76 @@
|
|
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
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# # on command line, from project's root dir
|
11
|
+
# $ watchr path/to/script
|
12
|
+
#
|
13
|
+
# See README for more details
|
14
|
+
#
|
15
|
+
module Watchr
|
16
|
+
autoload :Script, 'watchr/script'
|
17
|
+
autoload :Controller, 'watchr/controller'
|
18
|
+
|
19
|
+
module EventHandler
|
20
|
+
autoload :Base, 'watchr/event_handlers/base'
|
21
|
+
autoload :Unix, 'watchr/event_handlers/unix'
|
22
|
+
autoload :Portable, 'watchr/event_handlers/portable'
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
attr_accessor :options
|
27
|
+
attr_accessor :handler
|
28
|
+
|
29
|
+
# Options proxy.
|
30
|
+
#
|
31
|
+
# Currently supported options:
|
32
|
+
# * debug<Boolean> Debugging state. More verbose.
|
33
|
+
#
|
34
|
+
# ===== Examples
|
35
|
+
#
|
36
|
+
# Watchr.options.debug #=> false
|
37
|
+
# Watchr.options.debug = true
|
38
|
+
#
|
39
|
+
# ===== Returns
|
40
|
+
# options<Struct>:: options proxy.
|
41
|
+
#
|
42
|
+
#--
|
43
|
+
# On first use, initialize the options struct and default option values.
|
44
|
+
def options
|
45
|
+
@options ||= Struct.new(:debug).new
|
46
|
+
@options.debug ||= false
|
47
|
+
@options
|
48
|
+
end
|
49
|
+
|
50
|
+
# Outputs formatted debug statement to stdout, only if ::options.debug is true
|
51
|
+
#
|
52
|
+
# ===== Examples
|
53
|
+
#
|
54
|
+
# Watchr.options.debug = true
|
55
|
+
# Watchr.debug('im in ur codes, notifayinin u')
|
56
|
+
#
|
57
|
+
# outputs: "[watchr debug] im in ur codes, notifayinin u"
|
58
|
+
#
|
59
|
+
def debug(str)
|
60
|
+
puts "[watchr debug] #{str}" if options.debug
|
61
|
+
end
|
62
|
+
|
63
|
+
def handler
|
64
|
+
@handler ||=
|
65
|
+
#case ENV['HANDLER'] || RUBY_PLATFORM
|
66
|
+
case ENV['HANDLER'] || Config::CONFIG['host_os']
|
67
|
+
when /mswin|windows|cygwin/i
|
68
|
+
Watchr::EventHandler::Portable
|
69
|
+
when /bsd|sunos|solaris|darwin|osx|mach|linux/i, 'unix'
|
70
|
+
Watchr::EventHandler::Unix
|
71
|
+
else
|
72
|
+
Watchr::EventHandler::Portable
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Watchr
|
2
|
+
|
3
|
+
# The controller contains the app's core logic.
|
4
|
+
#
|
5
|
+
# ===== Examples
|
6
|
+
#
|
7
|
+
# script = Watchr::Script.new(file)
|
8
|
+
# contrl = Watchr::Controller.new(script)
|
9
|
+
# contrl.run
|
10
|
+
#
|
11
|
+
# Calling <tt>#run</tt> will enter the listening loop, and from then on every
|
12
|
+
# file event will trigger its corresponding action defined in <tt>script</tt>
|
13
|
+
#
|
14
|
+
# The controller also automatically adds the script's file itself to its list
|
15
|
+
# of monitored files and will detect any changes to it, providing on the fly
|
16
|
+
# updates of defined rules.
|
17
|
+
#
|
18
|
+
class Controller
|
19
|
+
|
20
|
+
# Creates a controller object around given <tt>script</tt>
|
21
|
+
#
|
22
|
+
# ===== Parameters
|
23
|
+
# script<Script>:: The script object
|
24
|
+
#
|
25
|
+
def initialize(script, handler)
|
26
|
+
@script = script
|
27
|
+
@handler = handler
|
28
|
+
@handler.add_observer(self)
|
29
|
+
|
30
|
+
Watchr.debug "using %s handler" % handler.class.name
|
31
|
+
end
|
32
|
+
|
33
|
+
# Enters listening loop.
|
34
|
+
#
|
35
|
+
# Will block control flow until application is explicitly stopped/killed.
|
36
|
+
#
|
37
|
+
def run
|
38
|
+
@handler.listen(monitored_paths)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Callback for file events.
|
42
|
+
#
|
43
|
+
# Called while control flow in in listening loop. It will execute the
|
44
|
+
# file's corresponding action as defined in the script. If the file is the
|
45
|
+
# script itself, it will refresh its state to account for potential changes.
|
46
|
+
#
|
47
|
+
# ===== Parameters
|
48
|
+
# path<Pathname, String>:: path that triggered event
|
49
|
+
# event<Symbol>:: event type (ignored for now)
|
50
|
+
#
|
51
|
+
def update(path, event = nil)
|
52
|
+
path = Pathname(path).expand_path
|
53
|
+
|
54
|
+
if path == @script.path
|
55
|
+
@script.parse!
|
56
|
+
@handler.refresh(monitored_paths)
|
57
|
+
else
|
58
|
+
@script.action_for(path).call
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# List of paths the script is monitoring.
|
63
|
+
#
|
64
|
+
# Basically this means all paths below current directoly recursivelly that
|
65
|
+
# match any of the rules' patterns, plus the script file.
|
66
|
+
#
|
67
|
+
# ===== Returns
|
68
|
+
# paths<Array[Pathname]>:: List of monitored paths
|
69
|
+
#
|
70
|
+
def monitored_paths
|
71
|
+
paths = Dir['**/*'].select do |path|
|
72
|
+
@script.patterns.any? {|p| path.match(p) }
|
73
|
+
end
|
74
|
+
paths.push(@script.path).compact!
|
75
|
+
paths.map {|path| Pathname(path).expand_path }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|