smparkes-watchr 0.5.7

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,25 @@
1
+ module Watchr
2
+ module EventHandler
3
+ module Unix
4
+
5
+ @defaults = []
6
+
7
+ class << self
8
+ attr_reader :defaults
9
+
10
+ def default
11
+ defaults.empty? &&
12
+ begin; require( 'watchr/event_handlers/rev' );
13
+ rescue LoadError => e; end
14
+ defaults.empty? &&
15
+ begin require( 'watchr/event_handlers/portable' );
16
+ defaults << Watchr::EventHandler::Portable;
17
+ end
18
+ defaults[0]
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,230 @@
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
+ DEFAULT_EVENT_TYPE = :modified
12
+
13
+ # Convenience type. Provides clearer and simpler access to rule properties.
14
+ #
15
+ # ===== Examples
16
+ #
17
+ # rule = script.watch('lib/.*\.rb') { 'ohaie' }
18
+ # rule.pattern #=> 'lib/.*\.rb'
19
+ # rule.action.call #=> 'ohaie'
20
+ #
21
+ Rule = Struct.new(:pattern, :event_types, :predicate, :action)
22
+
23
+ class Rule
24
+ def match path
25
+ ( md = self.pattern.match(path) ) &&
26
+ ( self.predicate == nil || self.predicate.call(md) )
27
+ end
28
+ end
29
+
30
+ # TODO eval context
31
+ class API #:nodoc:
32
+ end
33
+
34
+ # Creates a script object for <tt>path</tt>.
35
+ #
36
+ # Does not parse the script. The controller knows when to parse the script.
37
+ #
38
+ # ===== Parameters
39
+ # path<Pathname>:: the path to the script
40
+ #
41
+ def initialize(path)
42
+ @path = path
43
+ @rules = []
44
+ @default_action = lambda {}
45
+ end
46
+
47
+ # Main script API method. Builds a new rule, binding a pattern to an action.
48
+ #
49
+ # Whenever a file is saved that matches a rule's <tt>pattern</tt>, its
50
+ # corresponding <tt>action</tt> is triggered.
51
+ #
52
+ # Patterns can be either a Regexp or a string. Because they always
53
+ # represent paths however, it's simpler to use strings. But remember to use
54
+ # single quotes (not double quotes), otherwise escape sequences will be
55
+ # parsed (for example "foo/bar\.rb" #=> "foo/bar.rb", notice "\." becomes
56
+ # "."), and won't be interpreted as the regexp you expect.
57
+ #
58
+ # Also note that patterns will be matched against relative paths (relative
59
+ # from current working directory).
60
+ #
61
+ # Actions, the blocks passed to <tt>watch</tt>, receive a MatchData object
62
+ # as argument. It will be populated with the whole matched string (md[0])
63
+ # as well as individual backreferences (md[1..n]). See MatchData#[]
64
+ # documentation for more details.
65
+ #
66
+ # ===== Examples
67
+ #
68
+ # # in script file
69
+ # watch( 'test/test_.*\.rb' ) {|md| system("ruby #{md[0]}") }
70
+ # watch( 'lib/(.*)\.rb' ) {|md| system("ruby test/test_#{md[1]}.rb") }
71
+ #
72
+ # With these two rules, watchr will run any test file whenever it is itself
73
+ # changed (first rule), and will also run a corresponding test file
74
+ # whenever a lib file is changed (second rule).
75
+ #
76
+ # ===== Parameters
77
+ # pattern<~#match>:: pattern to match targetted paths
78
+ # event_types<Symbol|Array<Symbol>>::
79
+ # Rule will only match events of one of these type. Accepted types are :accessed,
80
+ # :modified, :changed, :delete and nil (any), where the first three
81
+ # correspond to atime, mtime and ctime respectively. Defaults to
82
+ # :modified.
83
+ # action<Block>:: action to trigger
84
+ #
85
+ # ===== Returns
86
+ # rule<Rule>:: rule created by the method
87
+ #
88
+ def watch(pattern, event_type = DEFAULT_EVENT_TYPE, predicate = nil, &action)
89
+ event_types = Array(event_type)
90
+ @rules << Rule.new(pattern, event_types, predicate, action || @default_action)
91
+ @rules.last
92
+ end
93
+
94
+ # Convenience method. Define a default action to be triggered when a rule
95
+ # has none specified.
96
+ #
97
+ # ===== Examples
98
+ #
99
+ # # in script file
100
+ #
101
+ # default_action { system('rake --silent rdoc') }
102
+ #
103
+ # watch( 'lib/.*\.rb' )
104
+ # watch( 'README.rdoc' )
105
+ # watch( 'TODO.txt' )
106
+ # watch( 'LICENSE' )
107
+ #
108
+ # # equivalent to:
109
+ #
110
+ # watch( 'lib/.*\.rb' ) { system('rake --silent rdoc') }
111
+ # watch( 'README.rdoc' ) { system('rake --silent rdoc') }
112
+ # watch( 'TODO.txt' ) { system('rake --silent rdoc') }
113
+ # watch( 'LICENSE' ) { system('rake --silent rdoc') }
114
+ #
115
+ def default_action(&action)
116
+ @default_action = action
117
+ end
118
+
119
+ # Eval content of script file.
120
+ #--
121
+ # TODO fix script file not found error
122
+ def parse!
123
+ Watchr.debug('loading script file %s' % @path.to_s.inspect)
124
+
125
+ reset
126
+
127
+ # Some editors do delete/rename. Even when they don't some events come very fast ...
128
+ # and editor could do a trunc/write. If you look after the trunc, before the write, well,
129
+ # things aren't pretty.
130
+
131
+ # Should probably use a watchdog timer that gets reset on every change and then only fire actions
132
+ # after the watchdog timer fires without get reset ..
133
+
134
+ sleep(0.1)
135
+
136
+ instance_eval(@path.read)
137
+
138
+ rescue Errno::ENOENT
139
+ # TODO figure out why this is happening. still can't reproduce
140
+ Watchr.debug('script file "not found". wth')
141
+ sleep(0.3) #enough?
142
+ instance_eval(@path.read)
143
+ end
144
+
145
+ # Find an action corresponding to a path and event type. The returned
146
+ # action is actually a wrapper around the rule's action, with the
147
+ # match_data prepopulated.
148
+ #
149
+ # ===== Params
150
+ # path<Pathnane,String>:: Find action that correspond to this path.
151
+ # event_type<Symbol>:: Find action only if rule's event if of this type.
152
+ #
153
+ # ===== Examples
154
+ #
155
+ # script.watch( 'test/test_.*\.rb' ) {|md| "ruby #{md[0]}" }
156
+ # script.action_for('test/test_watchr.rb').call #=> "ruby test/test_watchr.rb"
157
+ #
158
+ def call_action_for(path, event_type = DEFAULT_EVENT_TYPE)
159
+ path = rel_path(path).to_s
160
+ # p path
161
+ rules_for(path).each do |rule|
162
+ # p rule
163
+ rule.event_types.each do |rule_event_type|
164
+ # p rule_event_type
165
+ if ( rule_event_type.nil? && ( event_type != :load ) ) || ( rule_event_type == event_type )
166
+ data = path.match(rule.pattern)
167
+ return rule.action.call(data)
168
+ end
169
+ end
170
+ end
171
+ nil
172
+ end
173
+
174
+ # Collection of all patterns defined in script.
175
+ #
176
+ # ===== Returns
177
+ # patterns<String, Regexp>:: all patterns
178
+ #
179
+ def patterns
180
+ #@rules.every.pattern
181
+ @rules.map {|r| r.pattern }
182
+ end
183
+
184
+ def rules
185
+ @rules
186
+ end
187
+
188
+ # Path to the script file
189
+ #
190
+ # ===== Returns
191
+ # path<Pathname>:: path to script file
192
+ #
193
+ def path
194
+ Pathname(@path.respond_to?(:to_path) ? @path.to_path : @path.to_s).expand_path
195
+ end
196
+
197
+ private
198
+
199
+ # Rules corresponding to a given path, in reversed order of precedence
200
+ # (latest one is most inportant).
201
+ #
202
+ # ===== Parameters
203
+ # path<Pathname, String>:: path to look up rule for
204
+ #
205
+ # ===== Returns
206
+ # rules<Array(Rule)>:: rules corresponding to <tt>path</tt>
207
+ #
208
+ def rules_for(path)
209
+ @rules.reverse.select {|rule| path.match(rule.pattern) }
210
+ end
211
+
212
+ # Make a path relative to current working directory.
213
+ #
214
+ # ===== Parameters
215
+ # path<Pathname, String>:: absolute or relative path
216
+ #
217
+ # ===== Returns
218
+ # path<Pathname>:: relative path, from current working directory.
219
+ #
220
+ def rel_path(path)
221
+ Pathname(path).expand_path.relative_path_from(Pathname(Dir.pwd))
222
+ end
223
+
224
+ # Reset script state
225
+ def reset
226
+ @default_action = lambda {}
227
+ @rules.clear
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,70 @@
1
+ # Run me with:
2
+ #
3
+ # $ watchr manifest.watchr
4
+ #
5
+ # This script will remove a file from from the Manifest when it gets deleted,
6
+ # and will rebuild the Manifest on Ctrl-\
7
+ #
8
+ # Mostly serves as a demo for the :delete event type (and eventually for the
9
+ # :added event type). In reality this is much better implemented as a git
10
+ # post-commit script.
11
+ #
12
+
13
+ require 'pathname'
14
+ # --------------------------------------------------
15
+ # Helpers
16
+ # --------------------------------------------------
17
+ module Project
18
+ extend self
19
+ def files
20
+ `git ls-files --full-name`.strip.split($/).sort
21
+ end
22
+ end
23
+
24
+ class Manifest
25
+ attr_accessor :path
26
+
27
+ def initialize(path)
28
+ @path = Pathname(path).expand_path
29
+ create!
30
+ end
31
+
32
+ def remove(path)
33
+ paths = @path.read.strip.split($/)
34
+ @path.open('w') {|f| f << (paths - [path]).join("\n") }
35
+ end
36
+
37
+ def add(path)
38
+ paths = @path.read.strip.split($/)
39
+ @path.open('w') {|f| f << paths.push(path).sort.join("\n") }
40
+ end
41
+
42
+ private
43
+ def create!
44
+ File.open(@path.to_s, 'w') {} unless @path.exist?
45
+ end
46
+ end
47
+
48
+
49
+ @manifest = Manifest.new('Manifest')
50
+
51
+ # --------------------------------------------------
52
+ # Watchr Rules
53
+ # --------------------------------------------------
54
+ watch('.*', :deleted ) do |md|
55
+ @manifest.remove(md[0])
56
+ puts "removed #{md[0].inspect} from Manifest"
57
+ end
58
+
59
+ # --------------------------------------------------
60
+ # Signal Handling
61
+ # --------------------------------------------------
62
+ # Ctrl-\
63
+ Signal.trap('QUIT') do
64
+ puts " --- Updated Manifest ---\n"
65
+ @manifest.path.open('w') {|m| m << Project.files.join("\n").strip }
66
+ end
67
+
68
+ # Ctrl-C
69
+ Signal.trap('INT') { abort("\n") }
70
+
@@ -0,0 +1,38 @@
1
+ # Run me with:
2
+ #
3
+ # $ watchr specs.watchr
4
+
5
+ # --------------------------------------------------
6
+ # Convenience Methods
7
+ # --------------------------------------------------
8
+ def run(cmd)
9
+ puts(cmd)
10
+ system(cmd)
11
+ end
12
+
13
+ def run_all_tests
14
+ # see Rakefile for the definition of the test:all task
15
+ system( "rake -s test:all VERBOSE=true" )
16
+ end
17
+
18
+ # --------------------------------------------------
19
+ # Watchr Rules
20
+ # --------------------------------------------------
21
+ watch( '^test.*/test_.*\.rb' ) { |m| run( "ruby -rubygems %s" % m[0] ) }
22
+ watch( '^lib/(.*)\.rb' ) { |m| run( "ruby -rubygems test/test_%s.rb" % m[1] ) }
23
+ watch( '^lib/watchr/(.*)\.rb' ) { |m| run( "ruby -rubygems test/test_%s.rb" % m[1] ) }
24
+ watch( '^lib/watchr/event_handlers/(.*)\.rb' ) { |m| run( "ruby -rubygems test/event_handlers/test_%s.rb" % m[1] ) }
25
+ watch( '^test/test_helper\.rb' ) { run_all_tests }
26
+
27
+ # --------------------------------------------------
28
+ # Signal Handling
29
+ # --------------------------------------------------
30
+ # Ctrl-\
31
+ Signal.trap('QUIT') do
32
+ puts " --- Running all tests ---\n\n"
33
+ run_all_tests
34
+ end
35
+
36
+ # Ctrl-C
37
+ Signal.trap('INT') { abort("\n") }
38
+
@@ -0,0 +1,11 @@
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)
@@ -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,162 @@
1
+ require 'test/test_helper'
2
+
3
+ if false && HAVE_EM
4
+
5
+ class Watchr::EventHandler::Unix::SingleFileWatcher
6
+ public :type
7
+ end
8
+
9
+ class UnixEventHandlerTest < Test::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
+ @watcher.pathname.should be_kind_of(Pathname)
43
+ @watcher.pathname.to_s.should be(@watcher.path)
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
+ @watcher.instance_variable_get(:@reference_atime).should be(:time)
53
+ @watcher.instance_variable_get(:@reference_mtime).should be(:time)
54
+ @watcher.instance_variable_get(:@reference_ctime).should be(:time)
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
+ @watcher.type.should be(:accessed)
70
+
71
+ trigger_event @watcher, @now, :mtime
72
+ @watcher.type.should be(:modified)
73
+
74
+ trigger_event @watcher, @now, :ctime
75
+ @watcher.type.should be(:changed)
76
+
77
+ trigger_event @watcher, @now, :atime, :mtime
78
+ @watcher.type.should be(:modified)
79
+
80
+ trigger_event @watcher, @now, :mtime, :ctime
81
+ @watcher.type.should be(:modified)
82
+
83
+ trigger_event @watcher, @now, :atime, :ctime
84
+ @watcher.type.should be(:accessed)
85
+
86
+ trigger_event @watcher, @now, :atime, :mtime, :ctime
87
+ @watcher.type.should be(:modified)
88
+
89
+ @watcher.pathname.stubs(:exist?).returns(false)
90
+ @watcher.type.should be(:deleted)
91
+ end
92
+
93
+ ## monitoring file events
94
+
95
+ test "listens for events on monitored files" do
96
+ @handler.listen %w( foo bar )
97
+ @loop.watchers.size.should be(2)
98
+ @loop.watchers.every.path.should include('foo', 'bar')
99
+ @loop.watchers.every.class.uniq.should be([SingleFileWatcher])
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
+ @loop.watchers.size.should be(2)
135
+ @loop.watchers.every.path.should include('foo')
136
+ @loop.watchers.every.path.should include('bar')
137
+
138
+ @handler.refresh %w( baz bax )
139
+ @loop.watchers.size.should be(2)
140
+ @loop.watchers.every.path.should include('baz')
141
+ @loop.watchers.every.path.should include('bax')
142
+ @loop.watchers.every.path.should exclude('foo')
143
+ @loop.watchers.every.path.should exclude('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_EM