tekkub-watchr 0.5.0

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