observr 1.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,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
+