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.
- checksums.yaml +15 -0
- data/.gitignore +6 -0
- data/History.txt +32 -0
- data/LICENSE +19 -0
- data/README.md +119 -0
- data/Rakefile +34 -0
- data/TODO.md +31 -0
- data/bin/observr +105 -0
- data/contributions.txt +7 -0
- data/docs.observr +26 -0
- data/gem.observr +22 -0
- data/lib/observr.rb +133 -0
- data/lib/observr/controller.rb +87 -0
- data/lib/observr/event_handlers/base.rb +57 -0
- data/lib/observr/event_handlers/darwin.rb +160 -0
- data/lib/observr/event_handlers/portable.rb +75 -0
- data/lib/observr/event_handlers/unix.rb +125 -0
- data/lib/observr/script.rb +257 -0
- data/observr.gemspec +21 -0
- data/specs.watchr +37 -0
- data/test/README +11 -0
- data/test/event_handlers/test_base.rb +24 -0
- data/test/event_handlers/test_darwin.rb +111 -0
- data/test/event_handlers/test_portable.rb +142 -0
- data/test/event_handlers/test_unix.rb +162 -0
- data/test/test_controller.rb +121 -0
- data/test/test_helper.rb +37 -0
- data/test/test_script.rb +163 -0
- data/test/test_watchr.rb +76 -0
- metadata +114 -0
@@ -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
|
data/observr.gemspec
ADDED
@@ -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
|
data/specs.watchr
ADDED
@@ -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
|
data/test/README
ADDED
@@ -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
|
+
|