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.
@@ -31,8 +31,51 @@ describe Cool.io::Buffer do
31
31
  expect(buffer << "baz").to eq "baz"
32
32
  expect(buffer.read 3).to eq "arb"
33
33
  end
34
+
35
+ it "raises ArgumentError for a length below one" do
36
+ buffer << "foo"
37
+ expect { buffer.read 0 }.to raise_error ArgumentError
38
+ expect { buffer.read(-1) }.to raise_error ArgumentError
39
+ expect { buffer.read(-(2**100)) }.to raise_error ArgumentError
40
+ end
41
+
42
+ it "clamps a length which does not fit in a C int to the buffer size" do
43
+ buffer << "foobar"
44
+ expect(buffer.read 2**31).to eq "foobar"
45
+ expect(buffer.size).to eq 0
46
+ end
47
+
48
+ it "clamps lengths larger than an unsigned long or size_t to the buffer size" do
49
+ [2**32, 2**64, 2**100].each do |length|
50
+ buffer << "foobar"
51
+ expect(buffer.read(length)).to eq "foobar"
52
+ expect(buffer.size).to eq 0
53
+ end
54
+ end
55
+
56
+ it "keeps integer coercion for numeric and to_int lengths" do
57
+ length = Object.new
58
+ def length.to_int
59
+ 2
60
+ end
61
+ buffer << "foobar"
62
+ expect(buffer.read(length)).to eq "fo"
63
+ expect(buffer.read(2.9)).to eq "ob"
64
+ expect(buffer.to_str).to eq "ar"
65
+ end
34
66
  end
35
-
67
+
68
+ it "raises RangeError when to_str is called on a buffer past LONG_MAX",
69
+ :if => [0].pack("l!").bytesize == 4 do
70
+ long_max = 2**31 - 1
71
+ node_size = 16 * 1024 * 1024
72
+ big = Cool.io::Buffer.new(node_size)
73
+ chunk = "x" * node_size
74
+ ((long_max / node_size) + 1).times { big << chunk }
75
+ expect(big.size).to be > long_max
76
+ expect { big.to_str }.to raise_error(RangeError)
77
+ end
78
+
36
79
  describe "provides methods for performing non-blocking I/O" do
37
80
  require "tempfile"
38
81
 
@@ -126,6 +169,31 @@ describe Cool.io::Buffer do
126
169
  expect(buffer.size).to eq 0
127
170
  expect(buffer.empty?).to eq true
128
171
  end
172
+
173
+ it "reuses pooled nodes instead of leaking them" do
174
+ skip "GC.stat(:malloc_increase_bytes) is not available" unless GC.stat.key?(:malloc_increase_bytes)
175
+ node_size = Cool.io::Buffer.default_node_size
176
+ data = "a" * (node_size * 2 + 1)
177
+ cycle = proc do
178
+ buffer << data
179
+ buffer.clear
180
+ buffer << "x"
181
+ buffer.clear
182
+ end
183
+
184
+ # Marking a Buffer frees its pool, so keep GC off from the warm-up on.
185
+ GC.start
186
+ gc_was_disabled = GC.disable
187
+ begin
188
+ 3.times { cycle.call }
189
+ before = GC.stat(:malloc_increase_bytes)
190
+ 200.times { cycle.call }
191
+ grown = GC.stat(:malloc_increase_bytes) - before
192
+ ensure
193
+ GC.enable unless gc_was_disabled
194
+ end
195
+ expect(grown).to be < node_size * 4
196
+ end
129
197
  end
130
198
 
131
199
  context "#read_frame" do
@@ -142,6 +210,25 @@ describe Cool.io::Buffer do
142
210
  expect(data).to eq "foo\nbarbaz"
143
211
  expect(buffer.to_str).to eq ""
144
212
  end
213
+
214
+ it "raises TypeError instead of crashing when data is not a String" do
215
+ buffer << "hello world"
216
+ expect { buffer.read_frame 12345, " ".ord }.to raise_error(TypeError)
217
+ expect { buffer.read_frame nil, " ".ord }.to raise_error(TypeError)
218
+ expect { buffer.read_frame [], " ".ord }.to raise_error(TypeError)
219
+ end
220
+
221
+ it "raises FrozenError when data is a frozen String" do
222
+ buffer << "hello world"
223
+ expect { buffer.read_frame "frozen".freeze, " ".ord }.to raise_error(FrozenError)
224
+ end
225
+
226
+ it "coerces objects responding to #to_str" do
227
+ buffer << "foo\nbar"
228
+ convertible = Object.new
229
+ def convertible.to_str; +""; end
230
+ expect(buffer.read_frame convertible, "\n".ord).to eq true
231
+ end
145
232
  end
146
233
 
147
234
  end
data/spec/loop_spec.rb ADDED
@@ -0,0 +1,31 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'rbconfig'
3
+ require 'open3'
4
+
5
+ describe Cool.io::Loop do
6
+ it "ignores $LIBEV_FLAGS when skip_environment is true" do
7
+ script = <<~'RUBY'
8
+ require 'cool.io'
9
+ loop = Cool.io::Loop.new(skip_environment: true)
10
+ fired = false
11
+ timer = Cool.io::TimerWatcher.new(0.01)
12
+ timer.on_timer { fired = true; timer.detach }
13
+ timer.attach(loop)
14
+ loop.run
15
+ print fired
16
+ RUBY
17
+ lib = File.expand_path('../lib', __dir__)
18
+ # No CI platform has the Solaris event port backend, so honoring this would leave no backend.
19
+ env = { 'LIBEV_FLAGS' => Cool.io::Loop::EVBACKEND_PORT.to_s }
20
+ output, error, status = Open3.capture3(env, RbConfig.ruby, '-I', lib, '-e', script)
21
+ expect(status.success?).to eq(true), error
22
+ expect(output).to eq("true")
23
+ end
24
+
25
+ it "raises an error when no requested backend is usable" do
26
+ backend = RUBY_PLATFORM.include?("solaris") ? :kqueue : :port
27
+ expect {
28
+ Cool.io::Loop.new(skip_environment: true, backend: backend)
29
+ }.to raise_error(RuntimeError, /no usable backend/)
30
+ end
31
+ end
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../spec_helper', __FILE__)
2
+ require 'tmpdir'
2
3
 
3
4
  TEMP_FILE_PATH = "./test.txt"
4
5
 
@@ -18,12 +19,31 @@ class MyStatWatcher < Cool.io::StatWatcher
18
19
  end
19
20
  end
20
21
 
21
- def run_with_file_change(path)
22
+ def run_with_file_change(path, compact: false)
22
23
  reactor = Cool.io::Loop.new
23
24
 
24
25
  sw = MyStatWatcher.new(path)
25
26
  sw.attach(reactor)
26
27
 
28
+ # libev retains the path pointer passed to ev_stat_init() for the lifetime of
29
+ # the watcher. If the watcher held RSTRING_PTR(@path) directly, a compaction
30
+ # that relocates the @path String would leave libev dereferencing a stale
31
+ # address on every stat. Force a maximal relocation here so the watcher keeps
32
+ # operating against a path buffer it owns rather than Ruby-managed memory.
33
+ if compact
34
+ if GC.respond_to?(:verify_compaction_references)
35
+ # verify_compaction_references moves every movable object to a fresh slot.
36
+ # Its keyword arguments have varied across Ruby versions, so fall back.
37
+ begin
38
+ GC.verify_compaction_references(expand_heap: true, toplevel: true)
39
+ rescue ArgumentError
40
+ GC.verify_compaction_references(expand_heap: true)
41
+ end
42
+ elsif GC.respond_to?(:compact)
43
+ GC.compact
44
+ end
45
+ end
46
+
27
47
  tw = Cool.io::TimerWatcher.new(INTERVAL, true)
28
48
  tw.on_timer do
29
49
  reactor.stop if sw.accessed
@@ -69,6 +89,71 @@ describe Cool.io::StatWatcher do
69
89
  expect(watcher.previous.ino).to eq(watcher.current.ino)
70
90
  end
71
91
 
92
+ it "keeps firing on_change after GC compaction relocates objects" do
93
+ skip "GC compaction not available" unless GC.respond_to?(:compact)
94
+
95
+ watcher = run_with_file_change(TEMP_FILE_PATH, compact: true)
96
+ expect(watcher.accessed).to eq(true)
97
+ end
98
+
99
+ it "reports sizes and timestamps beyond the 32-bit range" do
100
+ Dir.mktmpdir do |dir|
101
+ path = File.join(dir, "large")
102
+ File.write(path, "a")
103
+ size = 2**32 + 1
104
+ time = Time.utc(2040, 1, 1)
105
+ current = nil
106
+ reactor = Cool.io::Loop.new
107
+ watcher = Cool.io::StatWatcher.new(path, INTERVAL)
108
+ watcher.define_singleton_method(:on_change) { |_, cur| current = cur }
109
+ watcher.attach(reactor)
110
+
111
+ File.truncate(path, size)
112
+ File.utime(time, time, path)
113
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
114
+ until (current && current.size == size && current.mtime == time) ||
115
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
116
+ reactor.run_once(0.1)
117
+ end
118
+ watcher.detach
119
+
120
+ expect(current.size).to eq(size)
121
+ expect(current.mtime).to eq(time)
122
+ end
123
+ end
124
+
125
+ it "reports blksize and blocks as File.lstat does" do
126
+ Dir.mktmpdir do |dir|
127
+ path = File.join(dir, "file")
128
+ File.write(path, "a")
129
+ size = 8192
130
+ current = nil
131
+ reactor = Cool.io::Loop.new
132
+ watcher = Cool.io::StatWatcher.new(path, INTERVAL)
133
+ watcher.define_singleton_method(:on_change) { |_, cur| current = cur }
134
+ watcher.attach(reactor)
135
+ begin
136
+ File.write(path, "a" * size)
137
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
138
+ until (current && current.size == size) ||
139
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
140
+ reactor.run_once(0.1)
141
+ end
142
+ ensure
143
+ watcher.detach
144
+ end
145
+
146
+ stat = File.lstat(path)
147
+ expect(current).not_to be_nil
148
+ expect(current.blksize).to eq(stat.blksize)
149
+ expect(current.blocks).to eq(stat.blocks)
150
+ end
151
+ end
152
+
153
+ it "raises ArgumentError when the path contains a null byte" do
154
+ expect { MyStatWatcher.new("foo\0bar") }.to raise_error(ArgumentError)
155
+ end
156
+
72
157
  it "should raise when the handler does not take 2 parameters" do
73
158
  class MyStatWatcher < Cool.io::StatWatcher
74
159
  remove_method :on_change
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../spec_helper', __FILE__)
2
+ require 'timeout'
2
3
 
3
4
  TIMEOUT = 0.010
4
5
  HOST = '127.0.0.1'
@@ -39,13 +40,31 @@ def on_message(data)
39
40
  @data = data
40
41
  end
41
42
 
43
+ # The reactor can be an order of magnitude slower under valgrind, so sleeping a
44
+ # fixed interval and assuming the event was processed by then makes these specs
45
+ # fail intermittently. Wait for the result instead. The timeout is generous
46
+ # enough that a slow machine still passes, and a genuine hang raises
47
+ # Timeout::Error here instead of blocking the suite forever.
48
+ WAIT_TIMEOUT = 5.0
49
+
50
+ def wait_until(timeout = WAIT_TIMEOUT)
51
+ Timeout.timeout(timeout) do
52
+ sleep 0.001 until yield
53
+ end
54
+ end
55
+
42
56
  def test_run(data = nil)
57
+ @data = ""
43
58
  reactor = Coolio::Loop.new
44
59
  server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
45
60
  reactor.attach(server)
46
61
  thread = Thread.new { reactor.run }
47
- send_data(data) if data
48
- sleep TIMEOUT
62
+ if data
63
+ send_data(data)
64
+ wait_until { @data == data }
65
+ else
66
+ sleep TIMEOUT
67
+ end
49
68
  reactor.stop
50
69
  server.detach
51
70
  send_data('') # to leave from blocking loop
@@ -86,6 +105,7 @@ ensure
86
105
  end
87
106
 
88
107
  def test_run_timeout(data = nil, timeout = TIMEOUT)
108
+ @data = ""
89
109
  reactor = Coolio::Loop.new
90
110
  server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
91
111
  reactor.attach(server)
@@ -95,8 +115,12 @@ def test_run_timeout(data = nil, timeout = TIMEOUT)
95
115
  reactor.run_once(timeout)
96
116
  end
97
117
  end
98
- send_data(data) if data
99
- sleep timeout
118
+ if data
119
+ send_data(data)
120
+ wait_until { @data == data }
121
+ else
122
+ sleep timeout
123
+ end
100
124
  server.detach
101
125
  running = false # another send is not required
102
126
  thread.join
@@ -151,9 +175,9 @@ describe Coolio::TCPServer do
151
175
  it "connected socket called on_connect" do
152
176
  begin
153
177
  connected = false
154
- server = Cool.io::TCPServer.new("localhost", port, ServerOnConnect, proc { connected = true })
178
+ server = Cool.io::TCPServer.new(HOST, port, ServerOnConnect, proc { connected = true })
155
179
  loop.attach server
156
- s = TCPSocket.open("localhost", port)
180
+ s = TCPSocket.open(HOST, port)
157
181
  loop.run_once
158
182
  s.close
159
183
  expect(connected).to eq true
@@ -177,9 +201,9 @@ describe Coolio::TCPServer do
177
201
  it "closed socket called on_close" do
178
202
  begin
179
203
  closed = false
180
- server = Cool.io::TCPServer.new("localhost", port, ServerOnConnect, proc { closed = true })
204
+ server = Cool.io::TCPServer.new(HOST, port, ServerOnConnect, proc { closed = true })
181
205
  loop.attach server
182
- s = TCPSocket.open("localhost", port)
206
+ s = TCPSocket.open(HOST, port)
183
207
  loop.run_once
184
208
  s.close
185
209
  loop.run_once
@@ -205,10 +229,10 @@ describe Coolio::TCPServer do
205
229
  it "server socket received data" do
206
230
  begin
207
231
  data = "aaa"
208
- server = Cool.io::TCPServer.new("localhost", port, Echo, proc { |d| data = d })
232
+ server = Cool.io::TCPServer.new(HOST, port, Echo, proc { |d| data = d })
209
233
  loop.attach server
210
234
  thread = Thread.new { loop.run }
211
- s = TCPSocket.open("localhost", port)
235
+ s = TCPSocket.open(HOST, port)
212
236
  s.write "zzz"
213
237
  sleep 0.1
214
238
  expect(data).to eq "zzz"
@@ -0,0 +1,54 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe 'Watcher reattachment' do
4
+ [:timer, :io].each do |type|
5
+ [false, true].each do |disabled|
6
+ [false, true].each do |same_loop|
7
+ it "reattaches a #{disabled ? 'disabled' : 'enabled'} #{type} watcher to #{same_loop ? 'the same' : 'another'} loop" do
8
+ original = Coolio::Loop.new
9
+ destination = same_loop ? original : Coolio::Loop.new
10
+ if type == :io
11
+ server = TCPServer.new('127.0.0.1', 0)
12
+ writer = TCPSocket.new('127.0.0.1', server.addr[1])
13
+ reader = server.accept
14
+ server.close
15
+ watcher = Coolio::IOWatcher.new(reader)
16
+ else
17
+ watcher = Coolio::TimerWatcher.new(0.01, true)
18
+ end
19
+ fired = false
20
+ callback = proc { fired = true }
21
+ if type == :io
22
+ watcher.on_readable(&callback)
23
+ else
24
+ watcher.on_timer(&callback)
25
+ end
26
+
27
+ watcher.attach(original)
28
+ watcher.disable if disabled
29
+ expect(watcher.attach(destination)).to equal(watcher)
30
+ expect(watcher.evloop).to equal(destination)
31
+ expect(watcher).to be_enabled
32
+ expect(destination.instance_variable_get(:@active_watchers)).to eq(1)
33
+ unless same_loop
34
+ expect(original.watchers).to be_empty
35
+ expect(original.instance_variable_get(:@active_watchers)).to eq(0)
36
+ end
37
+
38
+ writer.write('a') if writer
39
+ sleep 0.02 if type == :timer
40
+ destination.run_once(0.1)
41
+ expect(fired).to eq(true)
42
+ watcher.detach
43
+ expect(destination.watchers).to be_empty
44
+ expect(destination.instance_variable_get(:@active_watchers)).to eq(0)
45
+ ensure
46
+ watcher.detach if watcher && watcher.attached?
47
+ reader.close if reader
48
+ writer.close if writer
49
+ server.close if server && !server.closed?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,193 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require 'rbconfig'
5
+
6
+ describe 'Windows stat notification internals', :if => RUBY_PLATFORM =~ /mingw|mswin/ do
7
+ before(:context) do
8
+ @build = Dir.mktmpdir('coolio-stat-native-')
9
+ source = File.expand_path('fixtures/windows_stat_native.c', __dir__)
10
+ libev = File.expand_path('../ext/libev', __dir__)
11
+ FileUtils.cp(source, @build)
12
+ File.write(File.join(@build, 'extconf.rb'), <<~RUBY)
13
+ begin
14
+ require 'devkit' if RUBY_PLATFORM =~ /mingw/
15
+ rescue LoadError
16
+ # Other Windows toolchains can provide the compiler through PATH.
17
+ end
18
+ require 'mkmf'
19
+ $INCFLAGS << ' -I"#{libev}"'
20
+ abort 'ws2_32 is required' unless have_library('ws2_32')
21
+ create_makefile('coolio_stat_native')
22
+ RUBY
23
+ File.open(File.join(@build, 'extconf.rb'), 'a') do |file|
24
+ file.write <<~'RUBY'
25
+ # Match cool.io's Windows link order so Winsock calls resolve to ws2_32.
26
+ contents = File.read('Makefile')
27
+ contents.gsub!(/LIBS = (.*) (\S*ws2_32\S*)/i) do
28
+ "LIBS = #{Regexp.last_match(2)} #{Regexp.last_match(1)}"
29
+ end
30
+ File.write('Makefile', contents)
31
+ make = ENV['MAKE'] || RbConfig::CONFIG['MAKE'] || (RUBY_PLATFORM =~ /mswin/ ? 'nmake' : 'make')
32
+ abort 'native stat test build failed' unless system(make)
33
+ RUBY
34
+ end
35
+ output = IO.popen([RbConfig.ruby, 'extconf.rb'], chdir: @build, err: [:child, :out], &:read)
36
+ raise output unless $?.success?
37
+ end
38
+
39
+ after(:context) do
40
+ FileUtils.remove_entry(@build) if @build && File.directory?(@build)
41
+ end
42
+
43
+ def native_result(script, *args)
44
+ error_log = File.join(@build, 'native-error.log')
45
+ command = [RbConfig.ruby, '-I', @build, '-rcoolio_stat_native', '-e',
46
+ "$stdout.binmode; #{script}", *args]
47
+ output = IO.popen(command, 'rb', err: error_log, &:read)
48
+ expect($?.success?).to eq(true), File.read(error_log)
49
+ Marshal.load(output)
50
+ end
51
+
52
+ def native_paths(paths)
53
+ args = paths.flat_map { |path, parent, _| [path, parent ? '1' : '0'] }
54
+ native_result(<<~'RUBY', *args)
55
+ paths = ARGV.each_slice(2).map { |path, parent| CoolioStatNativeSpec.path(path, parent == '1') }
56
+ $stdout.write(Marshal.dump(paths))
57
+ RUBY
58
+ end
59
+
60
+ it 'preserves ordinary and extended-length drive roots' do
61
+ paths = [
62
+ ['C:\\file.txt', true, 'C:\\'],
63
+ ['C:\\', false, 'C:\\'],
64
+ ['C:\\', true, 'C:\\'],
65
+ ['\\\\?\\C:\\file.txt', true, '\\\\?\\C:\\'],
66
+ ['\\\\?\\C:\\', false, '\\\\?\\C:\\'],
67
+ ['\\\\?\\C:\\', true, '\\\\?\\C:\\'],
68
+ ['\\\\?\\C:\\dir\\file', true, '\\\\?\\C:\\dir']
69
+ ]
70
+ expect(native_paths(paths)).to eq(paths.map(&:last))
71
+ end
72
+
73
+ it 'preserves ordinary and extended-length UNC share roots' do
74
+ paths = [
75
+ ['\\\\server\\share\\', false, '\\\\server\\share\\'],
76
+ ['\\\\server\\share\\', true, '\\\\server\\share\\'],
77
+ ['\\\\server\\share\\file', true, '\\\\server\\share\\'],
78
+ ['\\\\?\\UNC\\server\\share\\', false, '\\\\?\\UNC\\server\\share\\'],
79
+ ['\\\\?\\UNC\\server\\share\\file', true, '\\\\?\\UNC\\server\\share\\']
80
+ ]
81
+ expect(native_paths(paths)).to eq(paths.map(&:last))
82
+ end
83
+
84
+ it 'falls back to polling on pipe failure and recovers without creating a second pipe' do
85
+ path = File.join(@build, 'watched.txt')
86
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.check_pipe(ARGV[0])))', path)
87
+ expect(result).to eq(true)
88
+ end
89
+
90
+ it 'destroys the loop without waiting for delayed worker completions' do
91
+ path = File.join(@build, 'shutdown.txt')
92
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.check_shutdown(ARGV[0])))', path)
93
+ expect(result).to eq(true)
94
+ end
95
+
96
+ [[0, 'polling'], [1, 'notifications'], [2, 'polling after an unrelated name change']].each do |mode, description|
97
+ it "checks a shared directory once without alias lookups during #{description}" do
98
+ result = native_result(<<~'RUBY', @build, mode.to_s)
99
+ result = CoolioStatNativeSpec.path_work(ARGV[0], ARGV[1].to_i)
100
+ $stdout.write(Marshal.dump(result))
101
+ RUBY
102
+ # One open and two identity queries for 32 changed files; no alias work.
103
+ expect(result).to eq([1, 2, 0, true])
104
+ end
105
+ end
106
+
107
+ it 'keeps finding each watch as watchers stop, restart and re-register' do
108
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.watch_index(ARGV[0])))', @build)
109
+ expect(result).to eq([1, 2, 0, true, true])
110
+ end
111
+
112
+ it 'shares directories and routes name records through the directory index' do
113
+ result = native_result(<<~'RUBY', @build)
114
+ dir = File.join(ARGV[0], 'a')
115
+ Dir.mkdir(dir) unless Dir.exist?(dir)
116
+ File.write(File.join(dir, "\u00e9.txt"), 'a')
117
+ $stdout.write(Marshal.dump(CoolioStatNativeSpec.name_index(ARGV[0])))
118
+ RUBY
119
+ expect(result).to eq([true,
120
+ [false, true, false, true, false, true, true],
121
+ [true, true, false, true, false, true, true]])
122
+ end
123
+
124
+ it 'relinks changed alias names and unlinks them when the watcher stops' do
125
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.alias_index(ARGV[0])))', @build)
126
+ expect(result).to eq([true, false, true, true, true])
127
+ end
128
+
129
+ it 'skips name lookups for roots and wildcard names' do
130
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.name_guards(ARGV[0])))', @build)
131
+ expect(result).to eq(root: true, wildcard: true)
132
+ end
133
+
134
+ it 'routes records for the short and the long name of a watched file' do
135
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.short_alias(ARGV[0])))', @build)
136
+ skip 'the volume does not create 8.3 names' if result.nil?
137
+ expect(result).to eq(short: true, long: true)
138
+ end
139
+
140
+ it 'keeps sharing directories, queues and routing names after the tables grow' do
141
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.index_growth(ARGV[0])))', @build)
142
+ expect(result).to eq([true, true, true, true])
143
+ end
144
+
145
+ [[1, 'polling'], [0, 'an explicit registration']].each do |polling, description|
146
+ it "keeps a shared child directory when a relative directory moves with the current directory during #{description}" do
147
+ first, second = %w[cwd1 cwd2].map { |name| File.join(@build, "#{name}-#{polling}") }
148
+ FileUtils.mkdir_p(File.join(first, 'rel'))
149
+ File.write(File.join(first, 'rel', 'f.txt'), 'a')
150
+ FileUtils.mkdir_p(File.join(second, 'rel'))
151
+ script = '$stdout.write(Marshal.dump(CoolioStatNativeSpec.children_cwd(ARGV[0], ARGV[1], ARGV[2].to_i)))'
152
+ result = native_result(script, first, second, polling.to_s)
153
+ expect(result).to eq(shared: true, failed: false, moved: false, followed: true)
154
+ end
155
+ end
156
+
157
+ def replaced_dir(mode)
158
+ script = '$stdout.write(Marshal.dump(CoolioStatNativeSpec.replaced_dir(ARGV[0], ARGV[1].to_i)))'
159
+ native_result(script, @build, mode.to_s)
160
+ end
161
+
162
+ it 'moves directory and file watchers to a directory recreated under the same name' do
163
+ expect(replaced_dir(0)).to eq(directory: true, file: true)
164
+ end
165
+
166
+ it 'moves the watchers when a rename record arrives before the creation record' do
167
+ expect(replaced_dir(1)).to eq(directory: true, file: true)
168
+ end
169
+
170
+ it 'registers the contents of a directory that was missing while the watcher registered again' do
171
+ expect(replaced_dir(2)).to eq(directory: true, file: nil)
172
+ end
173
+
174
+ it 'refreshes aliases after replacement records and retries failed lookups' do
175
+ path = File.join(@build, 'aliases.txt')
176
+ result = native_result('$stdout.write(Marshal.dump(CoolioStatNativeSpec.alias_recovery(ARGV[0])))', path)
177
+ expect(result).to eq([true, true, true, true])
178
+ end
179
+
180
+ it 'uses effective notification flags and respects EVFLAG_NOENV' do
181
+ path = File.join(@build, 'flags.txt')
182
+ # EVFLAG_NOINOTIFY = 0x00100000; EVFLAG_NOENV = 0x01000000.
183
+ [[0, 1048576, false], [16777216, 1048576, true],
184
+ [1048576, 0, true], [17825792, 0, false]].each do |flags, environment, expected|
185
+ result = native_result(<<~'RUBY', path, flags.to_s, environment.to_s)
186
+ ENV['LIBEV_FLAGS'] = ARGV[2]
187
+ enabled = CoolioStatNativeSpec.notification_enabled(ARGV[0], ARGV[1].to_i)
188
+ $stdout.write(Marshal.dump(enabled))
189
+ RUBY
190
+ expect(result).to eq(expected), "flags=#{flags}, LIBEV_FLAGS=#{environment}"
191
+ end
192
+ end
193
+ end