listen 2.7.5 → 2.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -0
  3. data/.rspec +0 -0
  4. data/.rubocop.yml +0 -0
  5. data/.travis.yml +0 -1
  6. data/.yardopts +0 -0
  7. data/CHANGELOG.md +0 -0
  8. data/CONTRIBUTING.md +0 -0
  9. data/Gemfile +25 -4
  10. data/Guardfile +0 -0
  11. data/LICENSE.txt +0 -0
  12. data/README.md +18 -10
  13. data/Rakefile +0 -0
  14. data/lib/listen.rb +2 -4
  15. data/lib/listen/adapter.rb +13 -4
  16. data/lib/listen/adapter/base.rb +33 -16
  17. data/lib/listen/adapter/bsd.rb +21 -38
  18. data/lib/listen/adapter/darwin.rb +17 -25
  19. data/lib/listen/adapter/linux.rb +34 -52
  20. data/lib/listen/adapter/polling.rb +9 -25
  21. data/lib/listen/adapter/tcp.rb +27 -14
  22. data/lib/listen/adapter/windows.rb +67 -23
  23. data/lib/listen/change.rb +26 -23
  24. data/lib/listen/cli.rb +0 -0
  25. data/lib/listen/directory.rb +47 -58
  26. data/lib/listen/file.rb +66 -101
  27. data/lib/listen/listener.rb +214 -155
  28. data/lib/listen/queue_optimizer.rb +104 -0
  29. data/lib/listen/record.rb +15 -5
  30. data/lib/listen/silencer.rb +14 -10
  31. data/lib/listen/tcp.rb +0 -1
  32. data/lib/listen/tcp/broadcaster.rb +31 -26
  33. data/lib/listen/tcp/message.rb +2 -2
  34. data/lib/listen/version.rb +1 -1
  35. data/listen.gemspec +1 -1
  36. data/spec/acceptance/listen_spec.rb +151 -239
  37. data/spec/acceptance/tcp_spec.rb +125 -134
  38. data/spec/lib/listen/adapter/base_spec.rb +13 -30
  39. data/spec/lib/listen/adapter/bsd_spec.rb +7 -35
  40. data/spec/lib/listen/adapter/darwin_spec.rb +18 -30
  41. data/spec/lib/listen/adapter/linux_spec.rb +49 -55
  42. data/spec/lib/listen/adapter/polling_spec.rb +20 -35
  43. data/spec/lib/listen/adapter/tcp_spec.rb +25 -27
  44. data/spec/lib/listen/adapter/windows_spec.rb +7 -33
  45. data/spec/lib/listen/adapter_spec.rb +10 -10
  46. data/spec/lib/listen/change_spec.rb +55 -57
  47. data/spec/lib/listen/directory_spec.rb +105 -155
  48. data/spec/lib/listen/file_spec.rb +186 -73
  49. data/spec/lib/listen/listener_spec.rb +233 -216
  50. data/spec/lib/listen/record_spec.rb +60 -22
  51. data/spec/lib/listen/silencer_spec.rb +48 -75
  52. data/spec/lib/listen/tcp/broadcaster_spec.rb +78 -69
  53. data/spec/lib/listen/tcp/listener_spec.rb +28 -71
  54. data/spec/lib/listen/tcp/message_spec.rb +48 -14
  55. data/spec/lib/listen_spec.rb +3 -3
  56. data/spec/spec_helper.rb +6 -3
  57. data/spec/support/acceptance_helper.rb +250 -31
  58. data/spec/support/fixtures_helper.rb +6 -4
  59. data/spec/support/platform_helper.rb +2 -2
  60. metadata +5 -5
  61. data/lib/listen/tcp/listener.rb +0 -108
@@ -1,45 +1,39 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Listen::TCP::Listener do
3
+ require 'listen/tcp/message'
4
+ require 'listen/tcp/broadcaster'
5
+
6
+ describe Listen::Listener do
4
7
 
5
8
  let(:host) { '10.0.0.2' }
6
9
  let(:port) { 4000 }
7
10
 
8
11
  subject { described_class.new("#{host}:#{port}", :recipient, options) }
9
12
  let(:options) { {} }
10
- let(:registry) { double(Celluloid::Registry, :[]= => true) }
13
+ let(:registry) { instance_double(Celluloid::Registry, :[]= => true) }
11
14
 
12
15
  let(:supervisor) do
13
- double(Celluloid::SupervisionGroup, add: true, pool: true)
16
+ instance_double(Celluloid::SupervisionGroup, add: true, pool: true)
14
17
  end
15
18
 
16
- let(:record) { double(Listen::Record, terminate: true, build: true) }
17
- let(:silencer) { double(Listen::Silencer, terminate: true) }
18
- let(:adapter) { double(Listen::Adapter::Base) }
19
- let(:broadcaster) { double(Listen::TCP::Broadcaster) }
20
- let(:change_pool) { double(Listen::Change, terminate: true) }
21
- let(:change_pool_async) { double('ChangePoolAsync') }
19
+ let(:record) { instance_double(Listen::Record, terminate: true, build: true) }
20
+ let(:silencer) { instance_double(Listen::Silencer, terminate: true) }
21
+ let(:adapter) { instance_double(Listen::Adapter::Base) }
22
+ let(:async) { instance_double(Listen::TCP::Broadcaster, broadcast: true) }
23
+ let(:broadcaster) { instance_double(Listen::TCP::Broadcaster, async: async) }
24
+ let(:change_pool) { instance_double(Listen::Change, terminate: true) }
25
+ let(:change_pool_async) { instance_double('ChangePoolAsync') }
22
26
  before do
23
- Celluloid::Registry.stub(:new) { registry }
24
- Celluloid::SupervisionGroup.stub(:run!) { supervisor }
25
- registry.stub(:[]).with(:silencer) { silencer }
26
- registry.stub(:[]).with(:adapter) { adapter }
27
- registry.stub(:[]).with(:record) { record }
28
- registry.stub(:[]).with(:change_pool) { change_pool }
29
- registry.stub(:[]).with(:broadcaster) { broadcaster }
27
+ allow(Celluloid::Registry).to receive(:new) { registry }
28
+ allow(Celluloid::SupervisionGroup).to receive(:run!) { supervisor }
29
+ allow(registry).to receive(:[]).with(:silencer) { silencer }
30
+ allow(registry).to receive(:[]).with(:adapter) { adapter }
31
+ allow(registry).to receive(:[]).with(:record) { record }
32
+ allow(registry).to receive(:[]).with(:change_pool) { change_pool }
33
+ allow(registry).to receive(:[]).with(:broadcaster) { broadcaster }
30
34
  end
31
35
 
32
36
  describe '#initialize' do
33
- its(:mode) { should be :recipient }
34
- its(:host) { should eq host }
35
- its(:port) { should eq port }
36
-
37
- it 'raises on invalid mode' do
38
- expect do
39
- described_class.new(port, :foo)
40
- end.to raise_error ArgumentError
41
- end
42
-
43
37
  it 'raises on omitted target' do
44
38
  expect do
45
39
  described_class.new(nil, :recipient)
@@ -50,21 +44,14 @@ describe Listen::TCP::Listener do
50
44
  context 'when broadcaster' do
51
45
  subject { described_class.new(port, :broadcaster) }
52
46
 
53
- it { should be_a_broadcaster }
54
- it { should_not be_a_recipient }
55
-
56
47
  it 'does not force TCP adapter through options' do
57
48
  expect(subject.options).not_to include(force_tcp: true)
58
49
  end
59
50
 
60
- context 'when host is omitted' do
61
- its(:host) { should be_nil }
62
- end
63
-
64
51
  describe '#start' do
65
52
  before do
66
- adapter.stub_chain(:async, :start)
67
- broadcaster.stub(:start)
53
+ allow(subject).to receive(:_start_adapter)
54
+ allow(broadcaster).to receive(:start)
68
55
  end
69
56
 
70
57
  it 'registers broadcaster' do
@@ -79,49 +66,27 @@ describe Listen::TCP::Listener do
79
66
  end
80
67
  end
81
68
 
82
- describe '#block' do
83
- let(:async) { double('TCP broadcaster async', broadcast: true) }
84
- let(:callback) { double(call: true) }
85
- let(:changes) do
86
- { modified: ['/foo'], added: [], removed: [] }
87
- end
88
-
69
+ describe 'queue' do
89
70
  before do
90
- broadcaster.stub(:async).and_return async
91
- end
92
-
93
- after do
94
- subject.block.call changes.values
95
- end
96
-
97
- context 'when paused' do
98
- it 'honours paused state and does nothing' do
99
- subject.pause
100
- expect(broadcaster).not_to receive(:async)
101
- expect(callback).not_to receive(:call)
102
- end
71
+ allow(broadcaster).to receive(:async).and_return async
103
72
  end
104
73
 
105
74
  context 'when stopped' do
106
75
  it 'honours stopped state and does nothing' do
107
76
  allow(subject).to receive(:supervisor) do
108
- double('SupervisionGroup', terminate: true)
77
+ instance_double(Celluloid::SupervisionGroup, terminate: true)
109
78
  end
110
79
 
111
80
  subject.stop
81
+ subject.queue(:file, :modified, 'foo')
112
82
  expect(broadcaster).not_to receive(:async)
113
- expect(callback).not_to receive(:call)
114
83
  end
115
84
  end
116
85
 
117
86
  it 'broadcasts changes asynchronously' do
118
- message = Listen::TCP::Message.new changes
87
+ message = Listen::TCP::Message.new(:file, :modified, 'foo', {})
119
88
  expect(async).to receive(:broadcast).with message.payload
120
- end
121
-
122
- it 'invokes original callback block' do
123
- subject.block = callback
124
- expect(callback).to receive(:call).with(*changes.values)
89
+ subject.queue(:file, :modified, 'foo')
125
90
  end
126
91
  end
127
92
  end
@@ -132,13 +97,5 @@ describe Listen::TCP::Listener do
132
97
  it 'forces TCP adapter through options' do
133
98
  expect(subject.options).to include(force_tcp: true)
134
99
  end
135
-
136
- it { should_not be_a_broadcaster }
137
- it { should be_a_recipient }
138
-
139
- context 'when host is omitted' do
140
- its(:host) { should eq described_class::DEFAULT_HOST }
141
- end
142
100
  end
143
-
144
101
  end
@@ -11,31 +11,65 @@ describe Listen::TCP::Message do
11
11
 
12
12
  describe '#initialize' do
13
13
  it 'initializes with an object' do
14
- message = described_class.new(object)
15
- expect(message.object).to be object
14
+ message = described_class.new(1, 2, 3)
15
+ expect(message.object).to eq [1, 2, 3]
16
16
  end
17
17
  end
18
18
 
19
19
  describe '#object=' do
20
- before do
21
- subject.object = object
20
+ subject do
21
+ described_class.new(object).tap do |message|
22
+ message.object = object
23
+ end
24
+ end
25
+
26
+ describe '#object' do
27
+ subject { super().object }
28
+ it { is_expected.to be object }
29
+ end
30
+
31
+ describe '#body' do
32
+ subject { super().body }
33
+ it { is_expected.to eq body }
34
+ end
35
+
36
+ describe '#size' do
37
+ subject { super().size }
38
+ it { is_expected.to eq size }
22
39
  end
23
40
 
24
- its(:object) { should be object }
25
- its(:body) { should eq body }
26
- its(:size) { should eq size }
27
- its(:payload) { should eq payload }
41
+ describe '#payload' do
42
+ subject { super().payload }
43
+ it { is_expected.to eq payload }
44
+ end
28
45
  end
29
46
 
30
47
  describe '#payload=' do
31
- before do
32
- subject.payload = payload
48
+ subject do
49
+ described_class.new(object).tap do |message|
50
+ message.payload = payload
51
+ end
52
+ end
53
+
54
+ describe '#object' do
55
+ subject { super().object }
56
+ it { is_expected.to eq object }
57
+ end
58
+
59
+ describe '#body' do
60
+ subject { super().body }
61
+ it { is_expected.to eq body }
62
+ end
63
+
64
+ describe '#size' do
65
+ subject { super().size }
66
+ it { is_expected.to eq size }
33
67
  end
34
68
 
35
- its(:object) { should eq object }
36
- its(:body) { should eq body }
37
- its(:size) { should eq size }
38
- its(:payload) { should be payload }
69
+ describe '#payload' do
70
+ subject { super().payload }
71
+ it { is_expected.to be payload }
72
+ end
39
73
  end
40
74
 
41
75
  describe '.from_buffer' do
@@ -9,7 +9,7 @@ describe Listen do
9
9
 
10
10
  context 'when using :forward_to option' do
11
11
  it 'initializes TCP-listener in broadcast-mode' do
12
- expect(Listen::TCP::Listener).to receive(:new).
12
+ expect(Listen::Listener).to receive(:new).
13
13
  with(4000, :broadcaster, '/path', {})
14
14
  described_class.to('/path', forward_to: 4000)
15
15
  end
@@ -18,7 +18,7 @@ describe Listen do
18
18
  it 'sets stopping at false' do
19
19
  allow(Listen::Listener).to receive(:new)
20
20
  Listen.to('/path')
21
- expect(Listen.stopping).to be_false
21
+ expect(Listen.stopping).to be_falsey
22
22
  end
23
23
  end
24
24
 
@@ -41,7 +41,7 @@ describe Listen do
41
41
 
42
42
  describe '.on' do
43
43
  it 'initializes TCP-listener in recipient-mode' do
44
- expect(Listen::TCP::Listener).to receive(:new).
44
+ expect(Listen::Listener).to receive(:new).
45
45
  with(4000, :recipient, '/path')
46
46
  described_class.on(4000, '/path')
47
47
  end
@@ -16,15 +16,18 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
16
16
 
17
17
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18
18
  RSpec.configure do |config|
19
- config.color_enabled = true
20
19
  config.order = :random
21
20
  config.filter_run focus: true
22
- config.treat_symbols_as_metadata_keys_with_true_values = true
23
21
  config.run_all_when_everything_filtered = true
24
- config.fail_fast = !ci?
22
+ # config.fail_fast = !ci?
25
23
  config.expect_with :rspec do |c|
26
24
  c.syntax = :expect
27
25
  end
26
+
27
+ config.mock_with :rspec do |mocks|
28
+ mocks.verify_doubled_constant_names = true
29
+ mocks.verify_partial_doubles = true
30
+ end
28
31
  end
29
32
 
30
33
  require 'rspec/retry'
@@ -1,47 +1,266 @@
1
- def listen(lag = 0.5)
2
- sleep lag # wait for changes
3
- sleep_until_next_second
4
- reset_changes
5
- yield
6
- sleep lag # wait for changes
7
- @changes
8
- end
1
+ {
2
+ modification: :modified,
3
+ addition: :added,
4
+ removal: :removed,
5
+ queued_modification: :modified,
6
+ queued_addition: :added,
7
+ }.each do |description, type|
9
8
 
10
- def setup_listener(options, callback)
11
- reset_changes
12
- Listen.to(paths, options, &callback)
13
- end
9
+ RSpec::Matchers.define "process_#{description}_of".to_sym do |expected|
10
+ match do |actual|
11
+ # Use cases:
12
+ # 1. reset the changes so they don't have leftovers
13
+ # 2. keep the queue if we're testing for existing accumulated changes
14
14
 
15
- def reset_changes
16
- @changes = { modified: [], added: [], removed: [] }
17
- end
15
+ # if were testing the queue (e.g. after unpause), don't reset
16
+ check_already_queued = /queued_/ =~ description
17
+ reset_queue = !check_already_queued
18
+
19
+ actual.listen(reset_queue) do
20
+ change_fs(type, expected) unless check_already_queued
21
+ end
22
+ actual.changes[type].include? expected
23
+ end
18
24
 
19
- def add_changes(type, changes)
20
- @changes[type] += relative_path(changes)
21
- @changes[type].uniq!
22
- @changes[type].sort!
25
+ failure_message do |actual|
26
+ result = actual.changes.inspect
27
+ "expected #{result} to include #{description} of #{expected}"
28
+ end
29
+
30
+ failure_message_when_negated do |actual|
31
+ result = actual.changes.inspect
32
+ "expected #{result} to not include #{description} of #{expected}"
33
+ end
34
+ end
23
35
  end
24
36
 
25
- def relative_path(changes)
26
- changes.map do |change|
27
- unfrozen_copy = change.dup
28
- [paths].flatten.each { |path| unfrozen_copy.gsub!(/#{path.to_s}\//, '') }
29
- unfrozen_copy
37
+ def change_fs(type, path)
38
+ case type
39
+ when :modified
40
+ unless File.exist?(path)
41
+ fail "Bad test: cannot modify #{path.inspect} (it doesn't exist)"
42
+ end
43
+
44
+ # wait until full second, because this might be followed by a modification
45
+ # event (which otherwise may not be detected every time)
46
+ _sleep_until_next_second(Pathname.pwd)
47
+
48
+ open(path, 'a') { |f| f.write('foo') }
49
+
50
+ # separate it from upcoming modifications"
51
+ _sleep_to_separate_events
52
+ when :added
53
+ if File.exist?(path)
54
+ fail "Bad test: cannot add #{path.inspect} (it already exists)"
55
+ end
56
+
57
+ # wait until full second, because this might be followed by a modification
58
+ # event (which otherwise may not be detected every time)
59
+ _sleep_until_next_second(Pathname.pwd)
60
+
61
+ open(path, 'w') { |f| f.write('foo') }
62
+
63
+ # separate it from upcoming modifications"
64
+ _sleep_to_separate_events
65
+ when :removed
66
+ unless File.exist?(path)
67
+ fail "Bad test: cannot remove #{path.inspect} (it doesn't exist)"
68
+ end
69
+ File.unlink(path)
70
+ else
71
+ fail "bad test: unknown type: #{type.inspect}"
30
72
  end
31
73
  end
32
74
 
33
- # Generates a small time difference before performing a time sensitive
34
- # task (like comparing mtimes of files).
75
+ # Used by change_fs() above so that the FS change (e.g. file created) happens
76
+ # as close to the start of a new second (time) as possible.
35
77
  #
36
- # @note Modification time for files only includes the milliseconds on Linux
37
- # with MRI > 1.9.2 and platform that support it (OS X 10.8 not included),
38
- # that's why we generate a difference that's greater than 1 second.
78
+ # E.g. if file is created at 1234567.999 (unix time), it's mtime on some
79
+ # filesystems is rounded, so it becomes 1234567.0, but if the change
80
+ # notification happens a little while later, e.g. at 1234568.111, now the file
81
+ # mtime and the current time in seconds are different (1234567 vs 1234568), and
82
+ # so the MD5 test won't kick in (see file.rb) - the file will not be considered
83
+ # for content checking (md5), so File.change will consider the file unmodified.
39
84
  #
40
- def sleep_until_next_second
41
- return unless darwin?
85
+ # This means, that if a file is added at 1234567.888 (and updated in Record),
86
+ # and then its content is modified at 1234567.999, and checking for changes
87
+ # happens at 1234568.111, the modification won't be detected.
88
+ # (because Record mtime is 1234567.0, current FS mtime from stat() is the
89
+ # same, and the checking happens in another second - 1234568).
90
+ #
91
+ # So basically, adding a file and detecting its later modification should all
92
+ # happen within 1 second (which makes testing and debugging difficult).
93
+ #
94
+ def _sleep_until_next_second(path)
95
+ Listen::File.inaccurate_mac_time?(path)
42
96
 
43
97
  t = Time.now
44
98
  diff = t.to_f - t.to_i
45
99
 
46
100
  sleep(1.05 - diff)
47
101
  end
102
+
103
+ # Special class to only allow changes within a specific time window
104
+
105
+ class TimedChanges
106
+ attr_reader :changes
107
+
108
+ def initialize
109
+ # Set to non-nil, because changes can immediately come after unpausing
110
+ # listener in an Rspec 'before()' block
111
+ @changes = { modified: [], added: [], removed: [] }
112
+ end
113
+
114
+ def change_offset
115
+ Time.now.to_f - @yield_time
116
+ end
117
+
118
+ def freeze_offset
119
+ result = @freeze_time - @yield_time
120
+ # Make an "almost zero" value more readable
121
+ result < 1e-4 ? 1e-4 : result
122
+ end
123
+
124
+ # Allow changes only during specific time wine
125
+ def allow_changes(reset_queue = true)
126
+ @freeze_time = nil
127
+ if reset_queue
128
+ # Clear to prepare for collecting new FS events
129
+ @changes = { modified: [], added: [], removed: [] }
130
+ else
131
+ # Since we're testing the queue and the listener callback is adding
132
+ # changes to the same hash (e.g. after a pause), copy the existing data
133
+ # to a new, unfrozen hash
134
+ @changes = @changes.dup if @changes.frozen?
135
+ @changes ||= { modified: [], added: [], removed: [] }
136
+ end
137
+
138
+ @yield_time = Time.now.to_f
139
+ yield
140
+ # Prevent recording changes after timeout
141
+ @changes.freeze
142
+ @freeze_time = Time.now.to_f
143
+ end
144
+ end
145
+
146
+ # Conveniently wrap a Listener instance for testing
147
+ class ListenerWrapper
148
+ attr_reader :listener, :changes
149
+ attr_accessor :lag
150
+
151
+ def initialize(callback, paths, *args)
152
+ # Lag depends mostly on wait_for_delay On Linux desktop, it's 0.06 - 0.11
153
+ #
154
+ # On Travis it used to be > 0.5, but that was before broadcaster sent
155
+ # changes immediately, so 0.2-0.4 might be enough for Travis, but we set it
156
+ # to 0.6
157
+ #
158
+ # The value should be 2-3 x wait_for_delay + time between fs operation and
159
+ # notification, which for polling and FSEvent means the configured latency
160
+ @lag = 0.6
161
+
162
+ @paths = paths
163
+
164
+ # Isolate collected changes between tests/listener instances
165
+ @timed_changes = TimedChanges.new
166
+
167
+ if callback
168
+ @listener = Listen.send(*args) do |modified, added, removed|
169
+ # Add changes to trigger frozen Hash error, making sure lag is enough
170
+ _add_changes(:modified, modified, changes)
171
+ _add_changes(:added, added, changes)
172
+ _add_changes(:removed, removed, changes)
173
+
174
+ unless callback == :track_changes
175
+ callback.call(modified, added, removed)
176
+ end
177
+ end
178
+ else
179
+ @listener = Listen.send(*args)
180
+ end
181
+ end
182
+
183
+ def changes
184
+ @timed_changes.changes
185
+ end
186
+
187
+ def listen(reset_queue = true)
188
+ @timed_changes.allow_changes(reset_queue) do
189
+
190
+ # give events time to be received, queued and processed
191
+ sleep lag
192
+
193
+ yield
194
+
195
+ sleep lag # wait for changes
196
+ end
197
+
198
+ # Keep this to detect a lag too small (changes during this sleep
199
+ # will trigger "frozen hash" error caught below (and displaying timeout
200
+ # details)
201
+ sleep 1
202
+
203
+ changes
204
+ end
205
+
206
+ private
207
+
208
+ def _add_changes(type, changes, dst)
209
+ dst[type] += _relative_path(changes)
210
+ dst[type].uniq!
211
+ dst[type].sort!
212
+
213
+ rescue RuntimeError => e
214
+ raise unless e.message == "can't modify frozen Hash"
215
+
216
+ # Show how by much the changes missed the timeout
217
+ change_offset = @timed_changes.change_offset
218
+ freeze_offset = @timed_changes.freeze_offset
219
+
220
+ msg = "Changes took #{change_offset}s (allowed lag: #{freeze_offset})s"
221
+
222
+ # Use STDERR (workaround for Celluloid, since it catches abort)
223
+ STDERR.puts msg
224
+ abort(msg)
225
+ end
226
+
227
+ def _relative_path(changes)
228
+ changes.map do |change|
229
+ unfrozen_copy = change.dup
230
+ [@paths].flatten.each do |path|
231
+ sub = path.sub(/\/$/, '').to_s
232
+ unfrozen_copy.gsub!(/^#{sub}\//, '')
233
+ end
234
+ unfrozen_copy
235
+ end
236
+ end
237
+ end
238
+
239
+ def setup_listener(options, callback = nil)
240
+ ListenerWrapper.new(callback, paths, :to, paths, options)
241
+ end
242
+
243
+ def setup_recipient(port, callback = nil)
244
+ ListenerWrapper.new(callback, paths, :on, port)
245
+ end
246
+
247
+ def _sleep_to_separate_events
248
+ # separate the events or Darwin and Polling
249
+ # will detect only the :added event
250
+ #
251
+ # (This is because both use directory scanning
252
+ # through Celluloid tasks, which may not kick in
253
+ # time before the next filesystem change)
254
+ #
255
+ # The minimum for this is the time it takes between a syscall
256
+ # changing the filesystem ... and ... an async
257
+ # Listen::File.scan to finish comparing the file with the
258
+ # Record
259
+ #
260
+ # This necessary for:
261
+ # - Darwin Adapter
262
+ # - Polling Adapter
263
+ # - Linux Adapter in FSEvent emulation mode
264
+ # - maybe Windows adapter (probably not)
265
+ sleep 0.4
266
+ end