cool.io 1.9.4 → 1.10.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 +4 -4
- data/.github/workflows/test.yaml +1 -0
- data/.github/workflows/valgrind.yaml +41 -0
- data/Rakefile +7 -19
- data/cool.io.gemspec +1 -3
- data/ext/cool.io/buffer.c +77 -52
- data/ext/cool.io/cool.io.h +6 -0
- data/ext/cool.io/ev_wrap.h +1 -0
- data/ext/cool.io/extconf.rb +0 -1
- data/ext/cool.io/iowatcher.c +18 -1
- data/ext/cool.io/loop.c +21 -19
- data/ext/cool.io/stat_watcher.c +44 -19
- data/ext/cool.io/timer_watcher.c +1 -1
- data/ext/cool.io/watcher.c +13 -1
- data/ext/cool.io/watcher.h +0 -17
- data/ext/libev/ev.c +80 -12
- data/ext/libev/ev_select.c +10 -0
- data/ext/libev/ev_vars.h +4 -0
- data/ext/libev/ev_win32_stat.c +851 -0
- data/ext/libev/ev_wrap.h +4 -0
- data/gems.rb +4 -0
- data/lib/cool.io/dns_resolver.rb +53 -8
- data/lib/cool.io/loop.rb +5 -1
- data/lib/cool.io/version.rb +2 -2
- data/libev_win_select.diff +25 -9
- data/spec/async_watcher_spec.rb +3 -1
- data/spec/detach_race_condition_spec.rb +59 -25
- data/spec/dns_spec.rb +197 -0
- data/spec/fixtures/windows_stat_native.c +1305 -0
- data/spec/iobuffer_spec.rb +88 -1
- data/spec/loop_spec.rb +31 -0
- data/spec/stat_watcher_spec.rb +86 -1
- data/spec/tcp_server_spec.rb +34 -10
- data/spec/watcher_reattach_spec.rb +54 -0
- data/spec/windows_stat_native_spec.rb +193 -0
- data/spec/windows_stat_watcher_spec.rb +363 -0
- metadata +14 -2
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'tmpdir'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'rbconfig'
|
|
5
|
+
|
|
6
|
+
describe 'Windows notification-backed StatWatcher', :if => RUBY_PLATFORM =~ /mingw|mswin/ do
|
|
7
|
+
before do
|
|
8
|
+
@directory = Dir.mktmpdir('coolio-notify-')
|
|
9
|
+
@loop = Coolio::Loop.new
|
|
10
|
+
@watchers = []
|
|
11
|
+
@threads = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
after do
|
|
15
|
+
@threads.each(&:join)
|
|
16
|
+
@watchers.each { |watcher| watcher.detach if watcher.attached? }
|
|
17
|
+
FileUtils.remove_entry(@directory)
|
|
18
|
+
@loop = nil
|
|
19
|
+
@watchers = nil
|
|
20
|
+
GC.start # exercise IOCP cancellation and loop destruction
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def watch(path, interval = 60)
|
|
24
|
+
watcher = Coolio::StatWatcher.new(path, interval)
|
|
25
|
+
watcher.attach(@loop)
|
|
26
|
+
@watchers << watcher
|
|
27
|
+
watcher
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def root_file
|
|
31
|
+
path = File.join(File.expand_path('/'), "coolio-#{File.basename(@directory)}")
|
|
32
|
+
begin
|
|
33
|
+
File.write(path, 'a')
|
|
34
|
+
rescue Errno::EACCES, Errno::EPERM, Errno::EROFS
|
|
35
|
+
skip 'requires write access to the drive root'
|
|
36
|
+
end
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# With a 60-second stat interval, a callback within this deadline must come
|
|
41
|
+
# from native notifications. Mutate from another thread while select waits.
|
|
42
|
+
def await_change(watcher, size: nil, exists: nil, &mutation)
|
|
43
|
+
change = nil
|
|
44
|
+
first_previous = nil
|
|
45
|
+
reactor = @loop
|
|
46
|
+
callback_thread = nil
|
|
47
|
+
stopped = false
|
|
48
|
+
stop = -> do
|
|
49
|
+
unless stopped
|
|
50
|
+
stopped = true
|
|
51
|
+
reactor.stop
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
watcher.define_singleton_method(:on_change) do |previous, current|
|
|
55
|
+
first_previous ||= previous
|
|
56
|
+
if (size.nil? || current.size == size) && (exists.nil? || (current.nlink != 0) == exists)
|
|
57
|
+
change = [first_previous, current]
|
|
58
|
+
callback_thread = Thread.current
|
|
59
|
+
stop.call
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
deadline = Coolio::TimerWatcher.new(2, false)
|
|
63
|
+
deadline.on_timer { stop.call }
|
|
64
|
+
deadline.attach(@loop)
|
|
65
|
+
@threads << Thread.new { sleep 0.1; mutation.call }
|
|
66
|
+
@loop.run
|
|
67
|
+
expect(change).not_to be_nil
|
|
68
|
+
expect(callback_thread).to eq(Thread.current)
|
|
69
|
+
change
|
|
70
|
+
ensure
|
|
71
|
+
deadline.detach if deadline && deadline.attached?
|
|
72
|
+
@threads.each(&:join)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'wakes a blocked loop for a file size change without waiting for polling' do
|
|
76
|
+
path = File.join(@directory, 'file')
|
|
77
|
+
File.write(path, 'a')
|
|
78
|
+
previous, current = await_change(watch(path), size: 6) { File.write(path, 'longer') }
|
|
79
|
+
expect(previous.size).to eq(1)
|
|
80
|
+
expect(current.size).to eq(6)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'notifies for creation, deletion, and recreation of a watched file' do
|
|
84
|
+
path = File.join(@directory, 'file')
|
|
85
|
+
watcher = watch(path)
|
|
86
|
+
previous, current = await_change(watcher, size: 7, exists: true) { File.write(path, 'created') }
|
|
87
|
+
expect(previous.nlink).to eq(0)
|
|
88
|
+
expect(current.nlink).not_to eq(0)
|
|
89
|
+
_, current = await_change(watcher, exists: false) { File.delete(path) }
|
|
90
|
+
expect(current.nlink).to eq(0)
|
|
91
|
+
_, current = await_change(watcher, size: 9, exists: true) { File.write(path, 'recreated') }
|
|
92
|
+
expect(current.size).to eq(9)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'notifies when a watched file is renamed away' do
|
|
96
|
+
path = File.join(@directory, 'file')
|
|
97
|
+
File.write(path, 'a')
|
|
98
|
+
_, current = await_change(watch(path)) { File.rename(path, "#{path}.old") }
|
|
99
|
+
expect(current.nlink).to eq(0)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'supports Unicode parent directories and filenames' do
|
|
103
|
+
directory = File.join(@directory, "\u76e3\u8996")
|
|
104
|
+
Dir.mkdir(directory)
|
|
105
|
+
path = File.join(directory, "\u5909\u66f4.txt")
|
|
106
|
+
File.write(path, 'a')
|
|
107
|
+
_, current = await_change(watch(path), size: 6) { File.write(path, 'longer') }
|
|
108
|
+
expect(current.size).to eq(6)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'matches notification names using Windows Unicode case folding' do
|
|
112
|
+
path = File.join(@directory, "\u00c4.txt")
|
|
113
|
+
File.write(path, 'a')
|
|
114
|
+
folded = File.join(@directory, "\u00e4.txt")
|
|
115
|
+
skip 'requires a case-insensitive directory' unless File.exist?(folded)
|
|
116
|
+
watcher = watch(folded)
|
|
117
|
+
_, current = await_change(watcher, size: 6) { File.write(path, 'longer') }
|
|
118
|
+
expect(current.size).to eq(6)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'notifies for an extended-length file path below a directory' do
|
|
122
|
+
path = File.join(@directory, 'file')
|
|
123
|
+
File.write(path, 'a')
|
|
124
|
+
extended = "\\\\?\\#{path.tr('/', '\\')}"
|
|
125
|
+
_, current = await_change(watch(extended), size: 6) { File.write(path, 'longer') }
|
|
126
|
+
expect(current.size).to eq(6)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'notifies for an extended-length file path at the drive root' do
|
|
130
|
+
path = root_file
|
|
131
|
+
extended = "\\\\?\\#{path.tr('/', '\\')}"
|
|
132
|
+
_, current = await_change(watch(extended), size: 6) { File.write(path, 'longer') }
|
|
133
|
+
expect(current.size).to eq(6)
|
|
134
|
+
ensure
|
|
135
|
+
File.delete(path) if path && File.exist?(path)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'notifies for an extended-length drive root directory' do
|
|
139
|
+
path = root_file
|
|
140
|
+
child = "#{path}.child"
|
|
141
|
+
root = "\\\\?\\#{File.expand_path('/').tr('/', '\\')}"
|
|
142
|
+
watcher = watch(root)
|
|
143
|
+
sleep 1.1 # Directory timestamps are exposed as whole seconds.
|
|
144
|
+
await_change(watcher) { File.write(child, 'a') }
|
|
145
|
+
ensure
|
|
146
|
+
[path, child].compact.each { |file| File.delete(file) if File.exist?(file) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'continues monitoring a shared directory after another watcher detaches' do
|
|
150
|
+
first = File.join(@directory, 'first')
|
|
151
|
+
second = File.join(@directory, 'second')
|
|
152
|
+
File.write(first, 'a')
|
|
153
|
+
File.write(second, 'a')
|
|
154
|
+
first_watcher = watch(first)
|
|
155
|
+
second_watcher = watch(second)
|
|
156
|
+
first_watcher.detach
|
|
157
|
+
_, current = await_change(second_watcher, size: 6) { File.write(second, 'longer') }
|
|
158
|
+
expect(current.size).to eq(6)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'transcodes a Windows-31J path for the wide Windows APIs' do
|
|
162
|
+
path = File.join(@directory, "\u5909\u66f4.txt")
|
|
163
|
+
File.write(path, 'a')
|
|
164
|
+
watcher = watch(path.encode('Windows-31J'))
|
|
165
|
+
_, current = await_change(watcher, size: 6) { File.write(path, 'longer') }
|
|
166
|
+
expect(current.size).to eq(6)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'transfers notification monitoring to another loop' do
|
|
170
|
+
path = File.join(@directory, 'file')
|
|
171
|
+
File.write(path, 'a')
|
|
172
|
+
watcher = watch(path)
|
|
173
|
+
old_loop = @loop
|
|
174
|
+
@loop = Coolio::Loop.new
|
|
175
|
+
watcher.attach(@loop)
|
|
176
|
+
old_loop.run_nonblock
|
|
177
|
+
_, current = await_change(watcher, size: 6) { File.write(path, 'longer') }
|
|
178
|
+
expect(current.size).to eq(6)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'does not dispatch a queued change after disable and supports re-enable' do
|
|
182
|
+
path = File.join(@directory, 'file')
|
|
183
|
+
File.write(path, 'a')
|
|
184
|
+
watcher = watch(path)
|
|
185
|
+
calls = 0
|
|
186
|
+
watcher.define_singleton_method(:on_change) { |_, _| calls += 1 }
|
|
187
|
+
File.write(path, 'queued')
|
|
188
|
+
sleep 0.05
|
|
189
|
+
watcher.disable
|
|
190
|
+
@loop.run_nonblock
|
|
191
|
+
expect(calls).to eq(0)
|
|
192
|
+
watcher.enable
|
|
193
|
+
_, current = await_change(watcher, size: 12) { File.write(path, 'after enable') }
|
|
194
|
+
expect(current.size).to eq(12)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'falls back to polling when the parent is missing, then registers notifications' do
|
|
198
|
+
directory = File.join(@directory, 'missing')
|
|
199
|
+
path = File.join(directory, 'file')
|
|
200
|
+
watcher = watch(path, 0.1)
|
|
201
|
+
_, current = await_change(watcher, size: 7, exists: true) { Dir.mkdir(directory); File.write(path, 'created') }
|
|
202
|
+
expect(current.size).to eq(7)
|
|
203
|
+
# Reattach with a long interval to prove the now-existing parent is watched.
|
|
204
|
+
watcher.detach
|
|
205
|
+
_, current = await_change(watch(path), size: 18) { File.write(path, 'longer than before') }
|
|
206
|
+
expect(current.size).to eq(18)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'handles directory entry changes' do
|
|
210
|
+
directory = File.join(@directory, 'directory')
|
|
211
|
+
Dir.mkdir(directory)
|
|
212
|
+
watcher = watch(directory)
|
|
213
|
+
# StatInfo exposes whole seconds for timestamps; cross a second boundary
|
|
214
|
+
# so this directory-only change is observable through the existing API.
|
|
215
|
+
sleep 1.1
|
|
216
|
+
await_change(watcher) { File.write(File.join(directory, 'child'), 'a') }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'recovers after a parent directory is moved and replaced' do
|
|
220
|
+
directory = File.join(@directory, 'parent')
|
|
221
|
+
Dir.mkdir(directory)
|
|
222
|
+
path = File.join(directory, 'file')
|
|
223
|
+
File.write(path, 'a')
|
|
224
|
+
watcher = watch(path, 0.1)
|
|
225
|
+
_, current = await_change(watcher, size: 11, exists: true) do
|
|
226
|
+
File.rename(directory, "#{directory}.old")
|
|
227
|
+
Dir.mkdir(directory)
|
|
228
|
+
File.write(path, 'replacement')
|
|
229
|
+
end
|
|
230
|
+
expect(current.size).to eq(11)
|
|
231
|
+
_, current = await_change(watcher, size: 5) { File.write(path, 'final') }
|
|
232
|
+
expect(current.size).to eq(5)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'keeps monitoring after an existing file is replaced' do
|
|
236
|
+
path = File.join(@directory, 'long-watched-filename.log')
|
|
237
|
+
replacement = File.join(@directory, 'replacement.log')
|
|
238
|
+
File.write(path, 'a')
|
|
239
|
+
File.write(replacement, 'replacement')
|
|
240
|
+
watcher = watch(path)
|
|
241
|
+
await_change(watcher, size: 11, exists: true) do
|
|
242
|
+
File.rename(path, "#{path}.old")
|
|
243
|
+
File.rename(replacement, path)
|
|
244
|
+
end
|
|
245
|
+
await_change(watcher, size: 12) { File.write(path, 'x', mode: 'a') }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'coalesces a burst of notifications and observes the final state' do
|
|
249
|
+
path = File.join(@directory, 'file')
|
|
250
|
+
File.write(path, 'a')
|
|
251
|
+
watcher = watch(path)
|
|
252
|
+
200.times do |index|
|
|
253
|
+
File.write(path, 'x' * (index + 2))
|
|
254
|
+
end
|
|
255
|
+
_, current = await_change(watcher, size: 5) { File.write(path, 'final') }
|
|
256
|
+
expect(current.size).to eq(5)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'validates a shared directory handle before attaching a new watcher' do
|
|
260
|
+
directory = File.join(@directory, 'parent')
|
|
261
|
+
Dir.mkdir(directory)
|
|
262
|
+
first = File.join(directory, 'first')
|
|
263
|
+
File.write(first, 'a')
|
|
264
|
+
watch(first)
|
|
265
|
+
|
|
266
|
+
File.rename(directory, "#{directory}.old")
|
|
267
|
+
Dir.mkdir(directory)
|
|
268
|
+
second = File.join(directory, 'second')
|
|
269
|
+
File.write(second, 'a')
|
|
270
|
+
_, current = await_change(watch(second), size: 5) { File.write(second, 'final') }
|
|
271
|
+
expect(current.size).to eq(5)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'keeps timers and Ruby threads responsive during sustained notifications' do
|
|
275
|
+
observed_size = nil
|
|
276
|
+
paths = 1000.times.map do |index|
|
|
277
|
+
path = File.join(@directory, "#{index}.log")
|
|
278
|
+
File.write(path, 'a')
|
|
279
|
+
watch(path).define_singleton_method(:on_change) do |_, current|
|
|
280
|
+
observed_size = current.size if index.zero?
|
|
281
|
+
end
|
|
282
|
+
path
|
|
283
|
+
end
|
|
284
|
+
clock = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
285
|
+
timer_gaps = []
|
|
286
|
+
thread_gaps = []
|
|
287
|
+
child = status = final_size = previous = deadline_at = nil
|
|
288
|
+
timer = Coolio::TimerWatcher.new(0.1, true)
|
|
289
|
+
timer.on_timer do
|
|
290
|
+
now = clock.call
|
|
291
|
+
timer_gaps << now - previous
|
|
292
|
+
previous = now
|
|
293
|
+
if child && (result = Process.wait2(child, Process::WNOHANG))
|
|
294
|
+
_, status = result
|
|
295
|
+
child = nil
|
|
296
|
+
final_size = File.size(paths.first)
|
|
297
|
+
end
|
|
298
|
+
# Process startup and notification delivery can outlast a fixed window
|
|
299
|
+
# on CI. Keep dispatching until the writer exits and its final size arrives.
|
|
300
|
+
@loop.stop if (status && observed_size == final_size) || now >= deadline_at
|
|
301
|
+
end
|
|
302
|
+
timer.attach(@loop)
|
|
303
|
+
@watchers << timer
|
|
304
|
+
done = false
|
|
305
|
+
# Use a separate process: a Ruby writer thread would itself be blocked by
|
|
306
|
+
# the GVL while the notification callback is draining the dirty set.
|
|
307
|
+
writer = <<~'RUBY'
|
|
308
|
+
path = ARGV.fetch(0)
|
|
309
|
+
sleep 0.3
|
|
310
|
+
clock = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
311
|
+
start = clock.call
|
|
312
|
+
writes = 0
|
|
313
|
+
while clock.call - start < 3
|
|
314
|
+
File.write(path, 'x', mode: 'a')
|
|
315
|
+
writes += 1
|
|
316
|
+
sleep [start + writes.fdiv(64) - clock.call, 0].max
|
|
317
|
+
end
|
|
318
|
+
RUBY
|
|
319
|
+
child = Process.spawn(RbConfig.ruby, '-e', writer, paths.first)
|
|
320
|
+
@threads << Thread.new do
|
|
321
|
+
last = clock.call
|
|
322
|
+
until done
|
|
323
|
+
sleep 0.05
|
|
324
|
+
now = clock.call
|
|
325
|
+
thread_gaps << now - last
|
|
326
|
+
last = now
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
previous = clock.call
|
|
330
|
+
deadline_at = previous + 15
|
|
331
|
+
@loop.run
|
|
332
|
+
done = true
|
|
333
|
+
@threads.each(&:join)
|
|
334
|
+
expect(status).not_to be_nil
|
|
335
|
+
expect(status.success?).to eq(true)
|
|
336
|
+
expect(timer_gaps.max).to be < 2
|
|
337
|
+
expect(thread_gaps.max).to be < 2
|
|
338
|
+
expect(final_size).to be > 1
|
|
339
|
+
expect(observed_size).to eq(final_size)
|
|
340
|
+
ensure
|
|
341
|
+
done = true
|
|
342
|
+
if child
|
|
343
|
+
begin
|
|
344
|
+
Process.kill('KILL', child)
|
|
345
|
+
rescue Errno::ESRCH
|
|
346
|
+
end
|
|
347
|
+
Process.wait(child)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'can repeatedly detach and reattach while changes are pending' do
|
|
352
|
+
path = File.join(@directory, 'file')
|
|
353
|
+
File.write(path, 'a')
|
|
354
|
+
watcher = watch(path)
|
|
355
|
+
50.times do |index|
|
|
356
|
+
File.write(path, 'x' * (index + 2))
|
|
357
|
+
watcher.detach
|
|
358
|
+
watcher.attach(@loop)
|
|
359
|
+
end
|
|
360
|
+
_, current = await_change(watcher, size: 5) { File.write(path, 'final') }
|
|
361
|
+
expect(current.size).to eq(5)
|
|
362
|
+
end
|
|
363
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cool.io
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tony Arcieri
|
|
@@ -77,6 +77,7 @@ extensions:
|
|
|
77
77
|
extra_rdoc_files: []
|
|
78
78
|
files:
|
|
79
79
|
- ".github/workflows/test.yaml"
|
|
80
|
+
- ".github/workflows/valgrind.yaml"
|
|
80
81
|
- ".gitignore"
|
|
81
82
|
- ".rspec"
|
|
82
83
|
- CHANGES.md
|
|
@@ -118,6 +119,7 @@ files:
|
|
|
118
119
|
- ext/libev/ev_select.c
|
|
119
120
|
- ext/libev/ev_vars.h
|
|
120
121
|
- ext/libev/ev_win32.c
|
|
122
|
+
- ext/libev/ev_win32_stat.c
|
|
121
123
|
- ext/libev/ev_wrap.h
|
|
122
124
|
- ext/libev/test_libev_win32.c
|
|
123
125
|
- gems.rb
|
|
@@ -142,7 +144,9 @@ files:
|
|
|
142
144
|
- spec/async_watcher_spec.rb
|
|
143
145
|
- spec/detach_race_condition_spec.rb
|
|
144
146
|
- spec/dns_spec.rb
|
|
147
|
+
- spec/fixtures/windows_stat_native.c
|
|
145
148
|
- spec/iobuffer_spec.rb
|
|
149
|
+
- spec/loop_spec.rb
|
|
146
150
|
- spec/spec_helper.rb
|
|
147
151
|
- spec/stat_watcher_spec.rb
|
|
148
152
|
- spec/tcp_server_spec.rb
|
|
@@ -151,6 +155,9 @@ files:
|
|
|
151
155
|
- spec/udp_socket_spec.rb
|
|
152
156
|
- spec/unix_listener_spec.rb
|
|
153
157
|
- spec/unix_server_spec.rb
|
|
158
|
+
- spec/watcher_reattach_spec.rb
|
|
159
|
+
- spec/windows_stat_native_spec.rb
|
|
160
|
+
- spec/windows_stat_watcher_spec.rb
|
|
154
161
|
homepage: https://github.com/socketry/cool.io
|
|
155
162
|
licenses:
|
|
156
163
|
- MIT
|
|
@@ -169,14 +176,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
169
176
|
- !ruby/object:Gem::Version
|
|
170
177
|
version: '0'
|
|
171
178
|
requirements: []
|
|
172
|
-
rubygems_version: 4.0.
|
|
179
|
+
rubygems_version: 4.0.20
|
|
173
180
|
specification_version: 4
|
|
174
181
|
summary: A cool framework for doing high performance I/O in Ruby
|
|
175
182
|
test_files:
|
|
176
183
|
- spec/async_watcher_spec.rb
|
|
177
184
|
- spec/detach_race_condition_spec.rb
|
|
178
185
|
- spec/dns_spec.rb
|
|
186
|
+
- spec/fixtures/windows_stat_native.c
|
|
179
187
|
- spec/iobuffer_spec.rb
|
|
188
|
+
- spec/loop_spec.rb
|
|
180
189
|
- spec/spec_helper.rb
|
|
181
190
|
- spec/stat_watcher_spec.rb
|
|
182
191
|
- spec/tcp_server_spec.rb
|
|
@@ -185,3 +194,6 @@ test_files:
|
|
|
185
194
|
- spec/udp_socket_spec.rb
|
|
186
195
|
- spec/unix_listener_spec.rb
|
|
187
196
|
- spec/unix_server_spec.rb
|
|
197
|
+
- spec/watcher_reattach_spec.rb
|
|
198
|
+
- spec/windows_stat_native_spec.rb
|
|
199
|
+
- spec/windows_stat_watcher_spec.rb
|