filewatcher 0.5.4 → 2.0.0.beta1

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,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ begin
6
+ require 'pry-byebug'
7
+ rescue LoadError
8
+ nil
9
+ end
10
+
11
+ require_relative 'spec_helper/watch_run'
12
+
13
+ class Filewatcher
14
+ ## Helper for common spec features between plugins
15
+ module SpecHelper
16
+ def logger
17
+ @logger ||= Logger.new($stdout, level: :debug)
18
+ end
19
+
20
+ def environment_specs_coefficients
21
+ @environment_specs_coefficients ||= {
22
+ -> { ENV['CI'] } => 1,
23
+ -> { RUBY_PLATFORM == 'java' } => 3,
24
+ -> { Gem::Platform.local.os == 'darwin' } => 1
25
+ }
26
+ end
27
+
28
+ def wait(seconds: 1, interval: 1, &block)
29
+ environment_specs_coefficients.each do |condition, coefficient|
30
+ next unless instance_exec(&condition)
31
+
32
+ interval *= coefficient
33
+ seconds *= coefficient
34
+ end
35
+
36
+ if block_given?
37
+ wait_with_block seconds, interval, &block
38
+ else
39
+ wait_without_block seconds
40
+ end
41
+ end
42
+
43
+ def wait_with_block(seconds, interval, &_block)
44
+ (seconds / interval).ceil.times do
45
+ break if yield
46
+
47
+ debug "sleep interval #{interval}"
48
+ sleep interval
49
+ end
50
+ end
51
+
52
+ def wait_without_block(seconds)
53
+ debug "sleep without intervals #{seconds}"
54
+ sleep seconds
55
+ end
56
+
57
+ def debug(string)
58
+ logger.debug "Thread ##{Thread.current.object_id} #{string}"
59
+ end
60
+
61
+ ## https://github.com/rubocop-hq/ruby-style-guide/issues/556#issuecomment-691274359
62
+ # rubocop:disable Style/ModuleFunction
63
+ extend self
64
+ # rubocop:enable Style/ModuleFunction
65
+ end
66
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Filewatcher
4
+ module SpecHelper
5
+ ## Base class for Filewatcher runners in specs
6
+ class WatchRun
7
+ extend Forwardable
8
+
9
+ TMP_DIR = "#{Dir.getwd}/spec/tmp"
10
+
11
+ def_delegators Filewatcher::SpecHelper, :debug, :wait
12
+
13
+ attr_reader :filename
14
+
15
+ def initialize(filename:, action:, directory:)
16
+ @filename =
17
+ if filename.match? %r{^(/|~|[A-Z]:)} then filename
18
+ else File.join(TMP_DIR, filename)
19
+ end
20
+ @directory = directory
21
+ @action = action
22
+ debug "action = #{action}"
23
+ end
24
+
25
+ def start
26
+ debug 'start'
27
+ File.write(@filename, 'content1') unless @action == :create
28
+
29
+ wait seconds: 1
30
+ end
31
+
32
+ def run(make_changes_times: 1)
33
+ start
34
+
35
+ make_changes_times.times do
36
+ make_changes
37
+
38
+ wait seconds: 2
39
+ end
40
+
41
+ stop
42
+ end
43
+
44
+ def stop
45
+ debug 'stop'
46
+ FileUtils.rm_r(@filename) if File.exist?(@filename)
47
+ end
48
+
49
+ private
50
+
51
+ def make_changes
52
+ debug "make changes, @action = #{@action}, @filename = #{@filename}"
53
+
54
+ if @action == :delete
55
+ FileUtils.remove(@filename)
56
+ elsif @directory
57
+ FileUtils.mkdir_p(@filename)
58
+ else
59
+ ## There is no `File.write` because of strange difference in parallel `File.mtime`
60
+ ## https://cirrus-ci.com/task/6107605053472768?command=test#L497-L511
61
+ system "echo 'content2' > #{@filename}"
62
+ debug_file_mtime
63
+ end
64
+
65
+ wait seconds: 1
66
+ end
67
+
68
+ def debug_file_mtime
69
+ debug "stat #{@filename}: #{Filewatcher.system_stat(@filename)}"
70
+ debug "File.mtime = #{File.mtime(@filename).strftime('%F %T.%9N')}"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Filewatcher
4
+ VERSION = '2.0.0.beta1'
5
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/filewatcher/snapshot'
4
+
5
+ describe Filewatcher::Snapshot do
6
+ let(:tmp_dir) { Filewatcher::SpecHelper::WatchRun::TMP_DIR }
7
+
8
+ let(:tmp_files) do
9
+ (1..4).map do |n|
10
+ File.join(tmp_dir, "file#{n}.txt")
11
+ end
12
+ end
13
+
14
+ def initialize_snapshot
15
+ described_class.new Dir[
16
+ File.join(tmp_dir, '**', '*')
17
+ ]
18
+ end
19
+
20
+ before do
21
+ FileUtils.mkdir_p tmp_dir
22
+
23
+ tmp_files.each_with_index do |tmp_file, n|
24
+ File.write tmp_file, "content#{n}"
25
+ end
26
+ end
27
+
28
+ after do
29
+ FileUtils.rm_r tmp_dir
30
+ end
31
+
32
+ describe '#initialize' do
33
+ subject(:snapshot) { initialize_snapshot }
34
+
35
+ it do
36
+ snapshot.each do |filename, snapshot_file|
37
+ expect(snapshot_file.mtime).to eq File.mtime(filename)
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '#-' do
43
+ subject(:difference) { second_snapshot - first_snapshot }
44
+
45
+ let(:first_snapshot) { initialize_snapshot }
46
+ let(:second_snapshot) { initialize_snapshot }
47
+ let(:new_file) { File.join(tmp_dir, 'file5.txt') }
48
+
49
+ before do
50
+ first_snapshot
51
+
52
+ sleep 0.1
53
+
54
+ File.write tmp_files[1], 'new content'
55
+ File.delete tmp_files[2]
56
+ File.write(new_file, 'new file')
57
+ end
58
+
59
+ it do
60
+ expect(difference).to eq(
61
+ tmp_files[1] => :updated,
62
+ tmp_files[2] => :deleted,
63
+ new_file => :created
64
+ )
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/filewatcher/version'
4
+
5
+ describe 'Filewatcher::VERSION' do
6
+ subject { Object.const_get(self.class.description) }
7
+
8
+ it { is_expected.to be_kind_of String }
9
+
10
+ it { is_expected.not_to be_empty }
11
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../lib/filewatcher'
5
+
6
+ describe Filewatcher do
7
+ subject(:processed) { watch_run.processed }
8
+
9
+ before do
10
+ FileUtils.mkdir_p tmp_dir
11
+ end
12
+
13
+ after do
14
+ logger.debug "FileUtils.rm_r #{tmp_dir}"
15
+ FileUtils.rm_r tmp_dir
16
+
17
+ Filewatcher::SpecHelper.wait seconds: 5, interval: 0.2 do
18
+ !File.exist?(tmp_dir)
19
+ end
20
+ end
21
+
22
+ def initialize_filewatcher(path, options = {})
23
+ described_class.new(path, options.merge(logger: logger))
24
+ end
25
+
26
+ let(:tmp_dir) { Filewatcher::SpecHelper::WatchRun::TMP_DIR }
27
+ let(:logger) { Filewatcher::SpecHelper.logger }
28
+
29
+ let(:filename) { 'tmp_file.txt' }
30
+ let(:action) { :update }
31
+ let(:directory) { false }
32
+ ## TODO: Check its needless
33
+ let(:every) { false }
34
+ let(:immediate) { false }
35
+ let(:interval) { 0.2 }
36
+ let(:filewatcher) do
37
+ initialize_filewatcher(
38
+ File.join(tmp_dir, '**', '*'), interval: interval, every: every, immediate: immediate
39
+ )
40
+ end
41
+
42
+ let(:watch_run) do
43
+ Filewatcher::SpecHelper::RubyWatchRun.new(
44
+ filename: filename, filewatcher: filewatcher, action: action,
45
+ directory: directory
46
+ )
47
+ end
48
+
49
+ let(:processed_files) { watch_run.processed.flat_map(&:keys) }
50
+
51
+ describe '#initialize' do
52
+ describe 'regular run' do
53
+ before { watch_run.run }
54
+
55
+ context 'with excluding selected file patterns' do
56
+ let(:filewatcher) do
57
+ initialize_filewatcher(
58
+ File.expand_path('spec/tmp/**/*'),
59
+ exclude: File.expand_path('spec/tmp/**/*.txt')
60
+ )
61
+ end
62
+
63
+ it { is_expected.to be_empty }
64
+ end
65
+
66
+ context 'with absolute paths including globs' do
67
+ let(:filewatcher) do
68
+ initialize_filewatcher(
69
+ File.expand_path('spec/tmp/**/*')
70
+ )
71
+ end
72
+
73
+ it { is_expected.to eq [{ watch_run.filename => :updated }] }
74
+ end
75
+
76
+ context 'with globs' do
77
+ let(:filewatcher) { initialize_filewatcher('spec/tmp/**/*') }
78
+
79
+ it { is_expected.to eq [{ watch_run.filename => :updated }] }
80
+ end
81
+
82
+ context 'with explicit relative paths with globs' do
83
+ let(:filewatcher) { initialize_filewatcher('./spec/tmp/**/*') }
84
+
85
+ it { is_expected.to eq [{ watch_run.filename => :updated }] }
86
+ end
87
+
88
+ context 'with explicit relative paths' do
89
+ let(:filewatcher) { initialize_filewatcher('./spec/tmp') }
90
+
91
+ it { is_expected.to eq [{ watch_run.filename => :updated }] }
92
+ end
93
+
94
+ context 'with tilde expansion' do
95
+ let(:filename) { File.expand_path('~/file_watcher_1.txt') }
96
+
97
+ let(:filewatcher) { initialize_filewatcher('~/file_watcher_1.txt') }
98
+
99
+ it { is_expected.to eq [{ filename => :updated }] }
100
+ end
101
+ end
102
+
103
+ describe '`:immediate` option' do
104
+ before do
105
+ watch_run.start
106
+ watch_run.stop
107
+ end
108
+
109
+ context 'when is `true`' do
110
+ let(:immediate) { true }
111
+
112
+ it { is_expected.to eq [{ '' => '' }] }
113
+
114
+ describe 'when watched' do
115
+ subject { watch_run.watched }
116
+
117
+ it { is_expected.to be > 0 }
118
+ end
119
+ end
120
+
121
+ context 'when is `false`' do
122
+ let(:immediate) { false }
123
+
124
+ it { is_expected.to be_empty }
125
+
126
+ describe 'when watched' do
127
+ subject { watch_run.watched }
128
+
129
+ it { is_expected.to eq 0 }
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ describe '#watch' do
136
+ before do
137
+ FileUtils.mkdir_p subdirectory if defined? subdirectory
138
+
139
+ watch_run.run
140
+ end
141
+
142
+ describe 'detecting file deletions' do
143
+ let(:action) { :delete }
144
+
145
+ it { is_expected.to eq [{ watch_run.filename => :deleted }] }
146
+ end
147
+
148
+ context 'when there are file additions' do
149
+ let(:action) { :create }
150
+
151
+ it { is_expected.to eq [{ watch_run.filename => :created }] }
152
+ end
153
+
154
+ context 'when there are file updates' do
155
+ let(:action) { :update }
156
+
157
+ it { is_expected.to eq [{ watch_run.filename => :updated }] }
158
+ end
159
+
160
+ context 'when there are new files in subdirectories' do
161
+ let(:subdirectory) { File.expand_path('spec/tmp/new_sub_directory') }
162
+
163
+ let(:filename) { File.join(subdirectory, 'file.txt') }
164
+ let(:action) { :create }
165
+ let(:every) { true }
166
+ ## https://github.com/filewatcher/filewatcher/pull/115#issuecomment-674581595
167
+ let(:interval) { 0.4 }
168
+
169
+ it do
170
+ expect(processed).to eq [
171
+ { subdirectory => :updated, watch_run.filename => :created }
172
+ ]
173
+ end
174
+ end
175
+
176
+ context 'when there are new subdirectories' do
177
+ let(:filename) { 'new_sub_directory' }
178
+ let(:directory) { true }
179
+ let(:action) { :create }
180
+
181
+ it { is_expected.to eq [{ watch_run.filename => :created }] }
182
+ end
183
+ end
184
+
185
+ describe '#stop' do
186
+ subject { watch_run.thread.join }
187
+
188
+ before do
189
+ watch_run.start
190
+ watch_run.filewatcher.stop
191
+ end
192
+
193
+ it { is_expected.to eq watch_run.thread }
194
+ end
195
+
196
+ def write_tmp_files(range)
197
+ logger.debug "#{__method__} #{range}"
198
+
199
+ directory = 'spec/tmp'
200
+ FileUtils.mkdir_p directory
201
+
202
+ range.to_a.map do |n|
203
+ File.write(file = "#{directory}/file#{n}.txt", "content#{n}")
204
+
205
+ Filewatcher::SpecHelper.wait seconds: 1
206
+
207
+ file
208
+ end
209
+ end
210
+
211
+ shared_context 'when paused' do
212
+ let(:action) { :create }
213
+ let(:every) { true }
214
+
215
+ before do
216
+ watch_run.start
217
+
218
+ logger.debug 'filewatcher.pause'
219
+ watch_run.filewatcher.pause
220
+
221
+ Filewatcher::SpecHelper.wait seconds: 1
222
+
223
+ write_tmp_files 1..4
224
+ end
225
+ end
226
+
227
+ describe '#pause' do
228
+ include_context 'when paused'
229
+
230
+ # update block should not have been called
231
+ it { is_expected.to be_empty }
232
+ end
233
+
234
+ describe '#resume' do
235
+ include_context 'when paused'
236
+
237
+ before do
238
+ logger.debug 'filewatcher.resume'
239
+ watch_run.filewatcher.resume
240
+ end
241
+
242
+ after do
243
+ watch_run.stop
244
+ end
245
+
246
+ describe 'changes while paused' do
247
+ # update block still should not have been called
248
+ it { is_expected.to be_empty }
249
+ end
250
+
251
+ describe 'changes after resumed' do
252
+ subject { processed_files }
253
+
254
+ let(:added_files) { write_tmp_files 5..7 }
255
+
256
+ before do
257
+ added_files
258
+
259
+ watch_run.wait
260
+
261
+ watch_run.filewatcher.stop
262
+ watch_run.stop
263
+ end
264
+
265
+ it { is_expected.to include_all_files added_files }
266
+ end
267
+ end
268
+
269
+ describe '#finalize' do
270
+ subject { processed_files }
271
+
272
+ let(:action) { :create }
273
+ let(:every) { true }
274
+
275
+ let(:added_files) { write_tmp_files 1..4 }
276
+
277
+ before do
278
+ watch_run.start
279
+ watch_run.filewatcher.stop
280
+ watch_run.thread.join
281
+
282
+ added_files
283
+
284
+ watch_run.filewatcher.finalize
285
+ end
286
+
287
+ it { is_expected.to include_all_files added_files }
288
+ end
289
+ end