em-dir-watcher 0.1.0 → 0.9.1

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,216 @@
1
+
2
+ require 'set'
3
+ require 'pathname'
4
+
5
+ module EMDirWatcher
6
+
7
+ class Entry
8
+
9
+ attr_reader :relative_path
10
+
11
+ def initialize tree, relative_path, ancestor_matches_inclusions
12
+ @tree = tree
13
+ @matches_inclusions = ancestor_matches_inclusions || @tree.includes?(relative_path)
14
+ @relative_path = relative_path
15
+ @is_file = false
16
+ @file_mtime = Time.at(0)
17
+ @entries = {}
18
+ end
19
+
20
+ def full_path
21
+ if @relative_path.empty? then @tree.full_path else File.join(@tree.full_path, @relative_path) end
22
+ end
23
+
24
+ def exists?
25
+ File.exists? full_path
26
+ end
27
+
28
+ def compute_is_file
29
+ FileTest.file? full_path
30
+ end
31
+
32
+ def compute_file_mtime
33
+ if FileTest.symlink?(full_path) then Time.at(0) else File.mtime(full_path) end
34
+ rescue Errno::ENOENT, Errno::ENOTDIR
35
+ Time.at(0)
36
+ end
37
+
38
+ def compute_entry_names
39
+ Dir.entries(full_path) - ['.', '..']
40
+ rescue Errno::ENOENT, Errno::ENOTDIR
41
+ []
42
+ end
43
+
44
+ def relative_path_of entry_name
45
+ if @relative_path.empty? then entry_name else File.join(@relative_path, entry_name) end
46
+ end
47
+
48
+ def compute_entries
49
+ new_entries = {}
50
+ compute_entry_names.each do |entry_name|
51
+ entry_relative_path = relative_path_of entry_name
52
+ next if @tree.excludes? entry_relative_path
53
+ new_entries[entry_name] = @entries[entry_name] || Entry.new(@tree, entry_relative_path, @matches_inclusions)
54
+ end
55
+ new_entries
56
+ end
57
+
58
+ def refresh_file! changed_relative_paths
59
+ if @matches_inclusions
60
+ used_to_be_file, @is_file = @is_file, compute_is_file
61
+ previous_file_mtime, @file_mtime = @file_mtime, compute_file_mtime
62
+ if used_to_be_file
63
+ changed_relative_paths << @relative_path if not @is_file or previous_file_mtime != @file_mtime
64
+ elsif @is_file
65
+ changed_relative_paths << @relative_path
66
+ end
67
+ end
68
+ end
69
+
70
+ def refresh! changed_relative_paths, refresh_subtree
71
+ refresh_file! changed_relative_paths
72
+ old_entries, @entries = @entries, compute_entries
73
+
74
+ if refresh_subtree
75
+ entries_to_refresh = Set.new(@entries.values) + Set.new(old_entries.values)
76
+ entries_to_refresh.each { |entry| entry.refresh! changed_relative_paths, true }
77
+ else
78
+ new_set, old_set = Set.new(@entries.values), Set.new(old_entries.values)
79
+ removed_entries, added_entries = old_set - new_set, new_set - old_set
80
+ still_existing_entries = old_set - removed_entries
81
+ added_or_removed_entries = added_entries + removed_entries
82
+
83
+ added_or_removed_entries.each { |entry| entry.refresh! changed_relative_paths, true }
84
+ still_existing_entries.each { |entry| entry.refresh_file! changed_relative_paths }
85
+ end
86
+ end
87
+
88
+ def scoped_refresh! changed_relative_paths, relative_scope, refresh_subtree
89
+ if relative_scope.size == 0
90
+ refresh! changed_relative_paths, refresh_subtree
91
+ else
92
+ entry_name, children_relative_scope = relative_scope[0], relative_scope[1..-1]
93
+ entry_relative_path = relative_path_of entry_name
94
+ return if @tree.excludes? entry_relative_path
95
+ entry = (@entries[entry_name] ||= Entry.new(@tree, entry_relative_path, @matches_inclusions))
96
+ entry.scoped_refresh! changed_relative_paths, children_relative_scope, refresh_subtree
97
+ if relative_scope.size == 1
98
+ @entries.delete entry_name unless entry.exists?
99
+ end
100
+ end
101
+ end
102
+
103
+ def recursive_file_entries
104
+ if @is_file
105
+ self
106
+ else
107
+ @entries.values.collect { |entry| entry.recursive_file_entries }.flatten
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ class RegexpMatcher
114
+ def initialize re
115
+ @re = re
116
+ end
117
+ def matches? relative_path
118
+ relative_path =~ @re
119
+ end
120
+ end
121
+
122
+ class PathGlobMatcher
123
+ def initialize glob
124
+ @glob = glob.gsub(%r-^/+-, '')
125
+ end
126
+ def matches? relative_path
127
+ File.fnmatch?(@glob, relative_path)
128
+ end
129
+ end
130
+
131
+ class NameGlobMatcher
132
+ def initialize glob
133
+ @glob = glob
134
+ end
135
+ def matches? relative_path
136
+ File.fnmatch?(@glob, File.basename(relative_path))
137
+ end
138
+ end
139
+
140
+ # Computes fine-grained (per-file) change events for a given directory tree,
141
+ # using coarse-grained (per-subtree) events for optimization.
142
+ class Tree
143
+
144
+ attr_reader :full_path
145
+ attr_reader :inclusions
146
+ attr_reader :exclusions
147
+
148
+ def self.parse_matchers matcher_expressions
149
+ matcher_expressions.collect do |expr|
150
+ if Regexp === expr
151
+ RegexpMatcher.new expr
152
+ elsif expr.include? '/'
153
+ PathGlobMatcher.new expr
154
+ else
155
+ NameGlobMatcher.new expr
156
+ end
157
+ end
158
+ end
159
+
160
+ def self.resolve_real_path_where_possible expanded_path
161
+ Pathname.new(expanded_path).realpath().to_s
162
+ rescue Errno::ENOENT
163
+ dirname, basename = File.dirname(expanded_path), File.basename(expanded_path)
164
+ return expanded_path if dirname == '/' || dirname == '.'
165
+ return File.join(resolve_real_path_where_possible(dirname), basename)
166
+ end
167
+
168
+ def initialize full_path, inclusions=nil, exclusions=[]
169
+ @full_path = self.class.resolve_real_path_where_possible(File.expand_path(full_path))
170
+ self.inclusions = inclusions
171
+ self.exclusions = exclusions
172
+ @root_entry = Entry.new self, '', false
173
+ @root_entry.refresh! [], true
174
+ end
175
+
176
+ def refresh! scope=nil, refresh_subtree=true
177
+ scope = self.class.resolve_real_path_where_possible(File.expand_path(scope || @full_path))
178
+
179
+ scope_with_slash = File.join(scope, '')
180
+ full_path_with_slash = File.join(@full_path, '')
181
+ return [] unless scope_with_slash.downcase[0..full_path_with_slash.size-1] == full_path_with_slash.downcase
182
+
183
+ relative_scope = (scope[full_path_with_slash.size..-1] || '').split('/')
184
+ relative_scope = relative_scope.reject { |item| item == '' }
185
+
186
+ changed_relative_paths = []
187
+ @root_entry.scoped_refresh! changed_relative_paths, relative_scope, refresh_subtree
188
+ changed_relative_paths.sort
189
+ end
190
+
191
+ def includes? relative_path
192
+ @inclusion_matchers.nil? || @inclusion_matchers.any? { |matcher| matcher.matches? relative_path }
193
+ end
194
+
195
+ def excludes? relative_path
196
+ @exclusion_matchers.any? { |matcher| matcher.matches? relative_path }
197
+ end
198
+
199
+ def full_file_list
200
+ @root_entry.recursive_file_entries.collect { |entry| entry.relative_path }.sort
201
+ end
202
+
203
+ private
204
+
205
+ def inclusions= new_inclusions
206
+ @inclusions = new_inclusions
207
+ @inclusion_matchers = new_inclusions && self.class.parse_matchers(new_inclusions)
208
+ end
209
+
210
+ def exclusions= new_exclusions
211
+ @exclusions = new_exclusions
212
+ @exclusion_matchers = self.class.parse_matchers(new_exclusions)
213
+ end
214
+
215
+ end
216
+ end
@@ -6,17 +6,12 @@ module EMDirWatcher
6
6
  PLATFORM = ENV['EM_DIR_WATCHER_PLATFORM'] ||
7
7
  case Config::CONFIG['target_os']
8
8
  when /mswin|mingw/ then 'Windows'
9
+ when /darwin/ then 'Mac'
10
+ when /linux/ then 'Linux'
9
11
  else 'NIX'
10
12
  end
11
13
  end
12
14
 
15
+ require "em-dir-watcher/tree"
13
16
  require "em-dir-watcher/platform/#{EMDirWatcher::PLATFORM.downcase}"
14
-
15
- module EMDirWatcher
16
- Watcher = Platform.const_get(PLATFORM)::Watcher
17
-
18
- def self.watch path, globs, &handler
19
- Watcher.new path, globs, &handler
20
- end
21
- end
22
-
17
+ require "em-dir-watcher/monitor"
data/test/helper.rb CHANGED
@@ -4,7 +4,15 @@ require 'shoulda'
4
4
 
5
5
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
6
  $LOAD_PATH.unshift(File.dirname(__FILE__))
7
- require 'em-directory-watch'
7
+ require 'em-dir-watcher'
8
+
9
+ TEST_DIR = if EMDirWatcher::PLATFORM == 'Windows' then 'c:/tmp/emdwtest' else '/tmp/emdwtest' end # Dir.mktmpdir
10
+ ALT_TEST_DIR = TEST_DIR + 'alt' # Dir.mktmpdir
8
11
 
9
12
  class Test::Unit::TestCase
13
+
14
+ def join list
15
+ list.join(", ").strip
16
+ end
17
+
10
18
  end
@@ -0,0 +1,161 @@
1
+ require 'helper'
2
+
3
+ class TestMonitor < Test::Unit::TestCase
4
+
5
+ # A sufficiently reliable maximum file system change reporting lag, in seconds.
6
+ # See README for explaination of its effect.
7
+ UNIT_DELAY = 0.5
8
+
9
+ def setup
10
+ FileUtils.rm_rf TEST_DIR
11
+ FileUtils.mkdir_p TEST_DIR
12
+ FileUtils.rm_rf ALT_TEST_DIR
13
+ FileUtils.mkdir_p ALT_TEST_DIR
14
+
15
+ FileUtils.mkdir File.join(TEST_DIR, 'bar')
16
+ FileUtils.mkdir File.join(TEST_DIR, 'bar', 'boo')
17
+
18
+ FileUtils.touch File.join(TEST_DIR, 'aa')
19
+ FileUtils.touch File.join(TEST_DIR, 'biz')
20
+ FileUtils.touch File.join(TEST_DIR, 'zz')
21
+ FileUtils.touch File.join(TEST_DIR, 'bar', 'foo')
22
+ FileUtils.touch File.join(TEST_DIR, 'bar', 'biz')
23
+ FileUtils.touch File.join(TEST_DIR, 'bar', 'biz.html')
24
+ FileUtils.touch File.join(TEST_DIR, 'bar', 'boo', 'bizzz')
25
+
26
+ @list = ['aa', 'biz', 'zz', 'bar/foo', 'bar/biz', 'bar/biz.html', 'bar/boo/bizzz'].sort
27
+ end
28
+
29
+ should "should report a deletion" do
30
+ all_changed_paths = []
31
+ stopped = false
32
+ watcher = nil
33
+ EM.run {
34
+ watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'] do |changed_paths|
35
+ all_changed_paths += changed_paths
36
+ end
37
+ watcher.when_ready_to_use do
38
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar')
39
+ EM.add_timer UNIT_DELAY do EM.stop end
40
+ end
41
+ }
42
+ watcher.stop
43
+ assert_equal join(['bar/foo', 'bar/biz', 'bar/boo/bizzz'].sort), join(all_changed_paths.sort)
44
+ end
45
+
46
+ should "choke on invalid option keys" do
47
+ assert_raise StandardError do
48
+ EM.run {
49
+ EMDirWatcher.watch TEST_DIR, :bogus_option => true
50
+ }
51
+ end
52
+ end
53
+
54
+ should "report each change individually when using a zero grace period" do
55
+ changed_1 = []
56
+ changed_2 = []
57
+ changed_cur = changed_1
58
+ stopped = false
59
+ watcher = nil
60
+ EM.run {
61
+ watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'] do |changed_paths|
62
+ changed_cur.push *changed_paths
63
+ end
64
+ watcher.when_ready_to_use do
65
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
66
+ EM.add_timer 1 do
67
+ changed_cur = changed_2
68
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'biz')
69
+ EM.add_timer 0.5 do EM.stop end
70
+ end
71
+ end
72
+ }
73
+ watcher.stop
74
+ assert_equal 'bar/foo >> bar/biz', join(changed_1.sort) + " >> " + join(changed_cur.sort)
75
+ end
76
+
77
+ should "combine changes when using a non-zero grace period" do
78
+ changed_1 = []
79
+ changed_2 = []
80
+ changed_cur = changed_1
81
+ stopped = false
82
+ watcher = nil
83
+ EM.run {
84
+ watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'], :grace_period => 2*UNIT_DELAY do |changed_paths|
85
+ changed_cur.push *changed_paths
86
+ end
87
+ watcher.when_ready_to_use do
88
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
89
+ EM.add_timer UNIT_DELAY do
90
+ changed_cur = changed_2
91
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'biz')
92
+ EM.add_timer 2*UNIT_DELAY do EM.stop end
93
+ end
94
+ end
95
+ }
96
+ watcher.stop
97
+ assert_equal ' >> bar/biz, bar/foo', join(changed_1.sort) + " >> " + join(changed_cur.sort)
98
+ end
99
+
100
+ should "not report duplicate changes when using a non-zero grace period" do
101
+ changed_1 = []
102
+ changed_2 = []
103
+ changed_cur = changed_1
104
+ stopped = false
105
+ watcher = nil
106
+ EM.run {
107
+ watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'], :grace_period => 3*UNIT_DELAY do |changed_paths|
108
+ changed_cur.push *changed_paths
109
+ end
110
+ watcher.when_ready_to_use do
111
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
112
+ EM.add_timer UNIT_DELAY do
113
+ FileUtils.touch File.join(TEST_DIR, 'bar', 'foo')
114
+ EM.add_timer UNIT_DELAY do
115
+ changed_cur = changed_2
116
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'foo')
117
+ FileUtils.rm_rf File.join(TEST_DIR, 'bar', 'biz')
118
+ EM.add_timer 2*UNIT_DELAY do EM.stop end
119
+ end
120
+ end
121
+ end
122
+ }
123
+ watcher.stop
124
+ assert_equal ' >> bar/biz, bar/foo', join(changed_1.sort) + " >> " + join(changed_cur.sort)
125
+ end
126
+
127
+ should "should report entire subtree as changed when a directory is moved away" do
128
+ all_changed_paths = []
129
+ stopped = false
130
+ watcher = nil
131
+ EM.run {
132
+ watcher = EMDirWatcher.watch TEST_DIR, :include_only => ['/bar'], :exclude => ['*.html'] do |changed_paths|
133
+ all_changed_paths += changed_paths
134
+ end
135
+ watcher.when_ready_to_use do
136
+ FileUtils.mv File.join(TEST_DIR, 'bar'), ALT_TEST_DIR
137
+ EM.add_timer UNIT_DELAY do EM.stop end
138
+ end
139
+ }
140
+ watcher.stop
141
+ assert_equal join(['bar/foo', 'bar/biz', 'bar/boo/bizzz'].sort), join(all_changed_paths.sort)
142
+ end
143
+
144
+ should "should report entire subtree as changed when a directory is moved in" do
145
+ all_changed_paths = []
146
+ stopped = false
147
+ watcher = nil
148
+ EM.run {
149
+ watcher = EMDirWatcher.watch ALT_TEST_DIR, :exclude => ['*.html'] do |changed_paths|
150
+ all_changed_paths += changed_paths
151
+ end
152
+ watcher.when_ready_to_use do
153
+ FileUtils.mv File.join(TEST_DIR, 'bar'), ALT_TEST_DIR
154
+ EM.add_timer UNIT_DELAY do EM.stop end
155
+ end
156
+ }
157
+ watcher.stop
158
+ assert_equal join(['bar/foo', 'bar/biz', 'bar/boo/bizzz'].sort), join(all_changed_paths.sort)
159
+ end
160
+
161
+ end