observr 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,257 @@
1
+ module Observr
2
+
3
+ # A script object wraps a script file, and is used by a controller.
4
+ #
5
+ # @example
6
+ #
7
+ # path = Pathname.new('specs.observr')
8
+ # script = Observr::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, observr 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
+ Observr.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_observr.rb').call #=> "ruby test/test_observr.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<String,Regexp>]
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<Rule>]
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
@@ -0,0 +1,21 @@
1
+ require './lib/observr'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "observr"
5
+ s.summary = "Modern continuous testing (flexible alternative to autotest)"
6
+ s.description = "Modern continuous 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 = "observr"
11
+ s.require_path = "lib"
12
+ s.bindir = "bin"
13
+ s.license = "MIT"
14
+ s.executables = "observr"
15
+ s.version = Observr::VERSION
16
+ s.files = `git ls-files`.strip.split("\n")
17
+
18
+ s.add_development_dependency 'minitest'
19
+ s.add_development_dependency 'mocha'
20
+ s.add_development_dependency 'every'
21
+ end
@@ -0,0 +1,37 @@
1
+ # Run me with:
2
+ # $ observr specs.observr
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/observr/(.*)\.rb' ) { |m| ruby "test/test_#{m[1]}.rb" }
10
+ watch( '^lib/observr/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
@@ -0,0 +1,11 @@
1
+
2
+ To use local observr executable for dev work:
3
+
4
+ $ ruby -rubygems -Ilib ./bin/observr -d specs.observr
5
+
6
+ To force a specific handler:
7
+
8
+ $ HANDLER=protable observr -d specs.observr
9
+ $ HANDLER=unix observr -d specs.observr
10
+
11
+ (see Observr.handler)
@@ -0,0 +1,24 @@
1
+ require 'test/test_helper'
2
+
3
+ class BaseEventHandlerTest < MiniTest::Unit::TestCase
4
+
5
+ class Handler
6
+ include Observr::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
@@ -0,0 +1,111 @@
1
+ require 'test/test_helper'
2
+
3
+ if Observr::HAVE_FSE
4
+
5
+ class Observr::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 Observr
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 Observr::HAVE_FSE
111
+