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.
@@ -0,0 +1,48 @@
1
+ require 'observer'
2
+
3
+ module Watchr
4
+ module EventHandler
5
+ class AbstractMethod < Exception; end
6
+
7
+ # Base functionality mixin meant to be included in specific event handlers.
8
+ module Base
9
+ include Observable
10
+
11
+ # Notify that a file was modified.
12
+ #
13
+ # ===== Parameters
14
+ # path<Pathname, String>:: full path or path relative to current working directory
15
+ # event<Symbol>:: event type (not yet used).
16
+ #
17
+ #--
18
+ # #changed and #notify_observers are Observable methods
19
+ def notify(path, event = nil)
20
+ changed(true)
21
+ notify_observers(path, event)
22
+ end
23
+
24
+ # Begin watching given paths and enter listening loop. Called by the controller.
25
+ #
26
+ # Abstract method
27
+ #
28
+ # ===== Parameters
29
+ # monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
30
+ #
31
+ def listen(monitored_paths)
32
+ raise AbstractMethod
33
+ end
34
+
35
+ # Called by the controller when the list of paths monitored by wantchr
36
+ # has changed. It should refresh the list of paths being watched.
37
+ #
38
+ # Abstract method
39
+ #
40
+ # ===== Parameters
41
+ # monitored_paths<Array(Pathname)>:: list of paths the application is currently monitoring.
42
+ #
43
+ def refresh(monitored_paths)
44
+ raise AbstractMethod
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ module Watchr
2
+ module EventHandler
3
+ class Portable
4
+ include Base
5
+
6
+ attr_accessor :monitored_paths
7
+ attr_accessor :reference_mtime
8
+
9
+ def initialize
10
+ @reference_mtime = Time.now
11
+ end
12
+
13
+ # Enters listening loop.
14
+ #
15
+ # Will block control flow until application is explicitly stopped/killed.
16
+ #
17
+ def listen(monitored_paths)
18
+ @monitored_paths = monitored_paths
19
+ loop { trigger; sleep(1) }
20
+ end
21
+
22
+ # See if an event occured, and if so notify observers.
23
+ def trigger #:nodoc:
24
+ path, type = detect_event
25
+ notify(path, type) unless path.nil?
26
+ end
27
+
28
+ # Update list of monitored paths.
29
+ def refresh(monitored_paths)
30
+ @monitored_paths = monitored_paths
31
+ end
32
+
33
+ private
34
+
35
+ # Verify mtimes of monitored files.
36
+ #
37
+ # If the latest mtime is more recent than the reference mtime, return
38
+ # that file's path.
39
+ #
40
+ # ===== Returns
41
+ # path and type of event if event occured, nil otherwise
42
+ #
43
+ def detect_event
44
+ path = @monitored_paths.max {|a,b| a.mtime <=> b.mtime }
45
+
46
+ if path.mtime > @reference_mtime
47
+ @reference_mtime = path.mtime
48
+ [path, :changed]
49
+ else
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,62 @@
1
+ require 'rev'
2
+
3
+ module Watchr
4
+ module EventHandler
5
+ class Unix
6
+ include Base
7
+
8
+ # Used by Rev. Wraps a monitored path, and Rev::Loop will call its
9
+ # callback on file events.
10
+ class SingleFileWatcher < Rev::StatWatcher #:nodoc:
11
+ class << self
12
+ # Stores a reference back to handler so we can call its #nofity
13
+ # method with file event info
14
+ attr_accessor :handler
15
+ end
16
+
17
+ # Callback. Called on file change event
18
+ # Delegates to Controller#update, passing in path and event type
19
+ def on_change
20
+ self.class.handler.notify(path, :changed)
21
+ end
22
+ end
23
+
24
+ def initialize
25
+ SingleFileWatcher.handler = self
26
+ @loop = Rev::Loop.default
27
+ end
28
+
29
+ # Enters listening loop.
30
+ #
31
+ # Will block control flow until application is explicitly stopped/killed.
32
+ #
33
+ def listen(monitored_paths)
34
+ @monitored_paths = monitored_paths
35
+ attach
36
+ @loop.run
37
+ end
38
+
39
+ # Rebuilds file bindings.
40
+ #
41
+ # will detach all current bindings, and reattach the <tt>monitored_paths</tt>
42
+ #
43
+ def refresh(monitored_paths)
44
+ @monitored_paths = monitored_paths
45
+ detach
46
+ attach
47
+ end
48
+
49
+ private
50
+
51
+ # Binds all <tt>monitored_paths</tt> to the listening loop.
52
+ def attach
53
+ @monitored_paths.each {|path| SingleFileWatcher.new(path.to_s).attach(@loop) }
54
+ end
55
+
56
+ # Unbinds all paths currently attached to listening loop.
57
+ def detach
58
+ @loop.watchers.each {|watcher| watcher.detach }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,192 @@
1
+ module Watchr
2
+
3
+ # A script object wraps a script file, and is used by a controller.
4
+ #
5
+ # ===== Examples
6
+ #
7
+ # path = Pathname.new('specs.watchr')
8
+ # script = Watchr::Script.new(path)
9
+ #
10
+ class Script
11
+
12
+ # Convenience type. Provides clearer and simpler access to rule properties.
13
+ #
14
+ # ===== Examples
15
+ #
16
+ # rule = script.watch('lib/.*\.rb') { 'ohaie' }
17
+ # rule.pattern #=> 'lib/.*\.rb'
18
+ # rule.action.call #=> 'ohaie'
19
+ #
20
+ Rule = Struct.new(:pattern, :action)
21
+
22
+ # TODO eval context
23
+ class API #:nodoc:
24
+ end
25
+
26
+ # Creates a script object for <tt>file</tt>.
27
+ #
28
+ # Will also immediatly parse the script so it is ready to be passed to a
29
+ # controller.
30
+ #
31
+ # ===== Parameters
32
+ # file<Pathname>:: the path to the script
33
+ #
34
+ # ===== TODO
35
+ # * only accept Pathname/String
36
+ #
37
+ #--
38
+ # see issue with #parse!
39
+ # (update class example when fixed)
40
+ #
41
+ def initialize(file = StringIO.new)
42
+ @file = file
43
+ @rules = []
44
+ @default_action = lambda {}
45
+ parse!
46
+ end
47
+
48
+ # Main script API method. Builds a new rule, binding a pattern to an action.
49
+ #
50
+ # Whenever a file is saved that matches a rule's <tt>pattern</tt>, its
51
+ # corresponding <tt>action</tt> is triggered.
52
+ #
53
+ # Patterns can be either a Regexp or a string. Because they always
54
+ # represent paths however, it's simpler to use strings. But remember to use
55
+ # single quotes (not double quotes), otherwise escape sequences will be
56
+ # parsed (for example "foo/bar\.rb" #=> "foo/bar.rb", notice "\." becomes
57
+ # "."), and won't be interpreted as the regexp you expect.
58
+ #
59
+ # Also note that patterns will be matched against relative paths (relative
60
+ # from current working directory).
61
+ #
62
+ # Actions, the blocks passed to <tt>watch</tt>, receive a MatchData object
63
+ # as argument. It will be populated with the whole matched string (md[0])
64
+ # as well as individual backreferences (md[1..n]). See MatchData#[]
65
+ # documentation for more details.
66
+ #
67
+ # ===== Examples
68
+ #
69
+ # # in script file
70
+ # watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
71
+ # watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") }
72
+ #
73
+ # With these two rules, watchr will run any test file whenever it is itself
74
+ # changed (first rule), and will also run a corresponding test file
75
+ # whenever a lib file is changed (second rule).
76
+ #
77
+ # ===== Parameters
78
+ # pattern<~#match>:: pattern to match targetted paths
79
+ # action<Block>:: action to trigger
80
+ #
81
+ # ===== Returns
82
+ # rule<Rule>:: rule created by the method
83
+ #
84
+ def watch(pattern, &action)
85
+ @rules << Rule.new(pattern, action || @default_action)
86
+ @rules.last
87
+ end
88
+
89
+ # Convenience method. Define a default action to be triggered when a rule
90
+ # has none specified.
91
+ #
92
+ # ===== Examples
93
+ #
94
+ # # in script file
95
+ #
96
+ # default_action { system('rake --silent rdoc') }
97
+ #
98
+ # watch( 'lib/.*\.rb' )
99
+ # watch( 'README.rdoc' )
100
+ # watch( 'TODO.txt' )
101
+ # watch( 'LICENSE' )
102
+ #
103
+ # # equivalent to:
104
+ #
105
+ # watch( 'lib/.*\.rb' ) { system('rake --silent rdoc') }
106
+ # watch( 'README.rdoc' ) { system('rake --silent rdoc') }
107
+ # watch( 'TODO.txt' ) { system('rake --silent rdoc') }
108
+ # watch( 'LICENSE' ) { system('rake --silent rdoc') }
109
+ #
110
+ def default_action(&action)
111
+ @default_action = action
112
+ end
113
+
114
+ # Eval content of script file.
115
+ #--
116
+ # TODO @file.read will only work with Pathname objects!
117
+ # TODO fix script file not found error
118
+ def parse!
119
+ Watchr.debug('loading script file %s' % @file.to_s.inspect)
120
+
121
+ @rules.clear
122
+ instance_eval(@file.read)
123
+
124
+ rescue Errno::ENOENT
125
+ # TODO figure out why this is happening. still can't reproduce
126
+ Watchr.debug('script file "not found". wth')
127
+ sleep(0.3) #enough?
128
+ instance_eval(@file.read)
129
+ end
130
+
131
+ # Find an action corresponding to a path. The returned action is actually a
132
+ # wrapper around the rule's action, with the match_data prepopulated.
133
+ #
134
+ # ===== Examples
135
+ #
136
+ # script.watch( 'test/test_.*\.rb' ) {|md| "ruby #{md[0]}" }
137
+ # script.action_for('test/test_watchr.rb').call #=> "ruby test/test_watchr.rb"
138
+ #
139
+ def action_for(path)
140
+ path = rel_path(path).to_s
141
+ rule = rule_for(path)
142
+ data = path.match(rule.pattern)
143
+ lambda { rule.action.call(data) }
144
+ end
145
+
146
+ # Collection of all patterns defined in script.
147
+ #
148
+ # ===== Returns
149
+ # patterns<String, Regexp>:: all patterns
150
+ #
151
+ def patterns
152
+ #@rules.every.pattern
153
+ @rules.map {|r| r.pattern }
154
+ end
155
+
156
+ # Path to the script file
157
+ #
158
+ # ===== Returns
159
+ # path<Pathname>:: path to script file
160
+ #
161
+ def path
162
+ Pathname(@file.respond_to?(:to_path) ? @file.to_path : @file.to_s).expand_path
163
+ end
164
+
165
+ private
166
+
167
+ # Rule corresponding to a given path. If more than one rule matches, then
168
+ # the last defined rule takes precedence.
169
+ #
170
+ # ===== Parameters
171
+ # path<Pathname, String>:: path to look up rule for
172
+ #
173
+ # ===== Returns
174
+ # rule<Rule>:: rule corresponding to <tt>path</tt>
175
+ #
176
+ def rule_for(path)
177
+ @rules.reverse.detect {|rule| path.match(rule.pattern) }
178
+ end
179
+
180
+ # Make a path relative to current working directory.
181
+ #
182
+ # ===== Parameters
183
+ # path<Pathname, String>:: absolute or relative path
184
+ #
185
+ # ===== Returns
186
+ # path<Pathname>:: relative path, from current working directory.
187
+ #
188
+ def rel_path(path)
189
+ Pathname(path).expand_path.relative_path_from(Pathname(Dir.pwd))
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,11 @@
1
+ module Watchr
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 5
5
+ TINY = 0
6
+ end
7
+
8
+ def self.version #:nodoc:
9
+ [VERSION::MAJOR, VERSION::MINOR, VERSION::TINY].join('.')
10
+ end
11
+ end
data/specs.watchr ADDED
@@ -0,0 +1,44 @@
1
+ # Run me with:
2
+ #
3
+ # $ watchr specs.watchr
4
+
5
+ # --------------------------------------------------
6
+ # Convenience Methods
7
+ # --------------------------------------------------
8
+ def all_test_files
9
+ Dir['test/**/test_*.rb'] - ['test/test_helper.rb']
10
+ end
11
+
12
+ def run(cmd)
13
+ puts(cmd)
14
+ system(cmd)
15
+ end
16
+
17
+ def run_all_tests
18
+ cmd = "ruby -rubygems -Ilib -e'%w( #{all_test_files.join(' ')} ).each {|file| require file }'"
19
+ run(cmd)
20
+ end
21
+
22
+ # --------------------------------------------------
23
+ # Watchr Rules
24
+ # --------------------------------------------------
25
+ watch( '^test.*/test_.*\.rb' ) { |m| run( "ruby -rubygems %s" % m[0] ) }
26
+ watch( '^lib/(.*)\.rb' ) { |m| run( "ruby -rubygems test/test_%s.rb" % m[1] ) }
27
+ watch( '^lib/watchr/(.*)\.rb' ) { |m| run( "ruby -rubygems test/test_%s.rb" % m[1] ) }
28
+ watch( '^lib/watchr/event_handlers/(.*)\.rb' ) { |m| run( "ruby -rubygems test/event_handlers/test_%s.rb" % m[1] ) }
29
+ watch( '^test/test_helper\.rb' ) { run_all_tests }
30
+
31
+ # --------------------------------------------------
32
+ # Signal Handling
33
+ # --------------------------------------------------
34
+ # Ctrl-\
35
+ Signal.trap('QUIT') do
36
+ puts " --- Running all tests ---\n\n"
37
+ run_all_tests
38
+ end
39
+
40
+ # Ctrl-C
41
+ Signal.trap('INT') { abort("\n") }
42
+
43
+
44
+ # vim:ft=ruby
@@ -0,0 +1,24 @@
1
+ require 'test/test_helper'
2
+
3
+ class BaseEventHandlerTest < Test::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
+ @handler.should respond_to(:notify)
15
+ @handler.should respond_to(:listen)
16
+ @handler.should respond_to(:refresh)
17
+ @handler.class.ancestors.should include(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
@@ -0,0 +1,58 @@
1
+ require 'test/test_helper'
2
+
3
+ class UnixEventHandlerTest < Test::Unit::TestCase
4
+ include Watchr
5
+
6
+ def setup
7
+ @handler = EventHandler::Portable.new
8
+ @handler.stubs(:loop)
9
+
10
+ @foo = Pathname('foo').expand_path
11
+ @bar = Pathname('bar').expand_path
12
+ @baz = Pathname('baz').expand_path
13
+ @bax = Pathname('bax').expand_path
14
+
15
+ @foo.stubs(:mtime).returns(Time.now - 100)
16
+ @bar.stubs(:mtime).returns(Time.now - 100)
17
+ @baz.stubs(:mtime).returns(Time.now - 100)
18
+ @bax.stubs(:mtime).returns(Time.now - 100)
19
+ end
20
+
21
+ test "triggers listening state" do
22
+ @handler.expects(:loop)
23
+ @handler.listen([])
24
+ end
25
+
26
+ ## monitoring file events
27
+
28
+ test "listens for events on monitored files" do
29
+ @handler.listen [ @foo, @bar ]
30
+ @handler.monitored_paths.should include(@foo)
31
+ @handler.monitored_paths.should include(@bar)
32
+ end
33
+
34
+ test "notifies observers on file event" do
35
+ @foo.stubs(:mtime).returns(Time.now + 100) # fake event
36
+
37
+ @handler.listen [ @foo, @bar ]
38
+ @handler.expects(:notify).with(@foo, :changed)
39
+ @handler.trigger
40
+ end
41
+
42
+ test "doesn't trigger on start" do
43
+ end
44
+
45
+ ## on the fly updates of monitored files list
46
+
47
+ test "reattaches to new monitored files" do
48
+ @handler.listen [ @foo, @bar ]
49
+ @handler.monitored_paths.should include(@foo)
50
+ @handler.monitored_paths.should include(@bar)
51
+
52
+ @handler.refresh [ @baz, @bax ]
53
+ @handler.monitored_paths.should include(@baz)
54
+ @handler.monitored_paths.should include(@bax)
55
+ @handler.monitored_paths.should exclude(@foo)
56
+ @handler.monitored_paths.should exclude(@bar)
57
+ end
58
+ end