cool.io 1.9.5 → 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.
@@ -36,6 +36,7 @@ describe Cool.io::Buffer do
36
36
  buffer << "foo"
37
37
  expect { buffer.read 0 }.to raise_error ArgumentError
38
38
  expect { buffer.read(-1) }.to raise_error ArgumentError
39
+ expect { buffer.read(-(2**100)) }.to raise_error ArgumentError
39
40
  end
40
41
 
41
42
  it "clamps a length which does not fit in a C int to the buffer size" do
@@ -43,8 +44,38 @@ describe Cool.io::Buffer do
43
44
  expect(buffer.read 2**31).to eq "foobar"
44
45
  expect(buffer.size).to eq 0
45
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
46
66
  end
47
-
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
+
48
79
  describe "provides methods for performing non-blocking I/O" do
49
80
  require "tempfile"
50
81
 
@@ -138,6 +169,31 @@ describe Cool.io::Buffer do
138
169
  expect(buffer.size).to eq 0
139
170
  expect(buffer.empty?).to eq true
140
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
141
197
  end
142
198
 
143
199
  context "#read_frame" do
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
 
@@ -95,6 +96,60 @@ describe Cool.io::StatWatcher do
95
96
  expect(watcher.accessed).to eq(true)
96
97
  end
97
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
+
98
153
  it "raises ArgumentError when the path contains a null byte" do
99
154
  expect { MyStatWatcher.new("foo\0bar") }.to raise_error(ArgumentError)
100
155
  end
@@ -175,9 +175,9 @@ describe Coolio::TCPServer do
175
175
  it "connected socket called on_connect" do
176
176
  begin
177
177
  connected = false
178
- server = Cool.io::TCPServer.new("localhost", port, ServerOnConnect, proc { connected = true })
178
+ server = Cool.io::TCPServer.new(HOST, port, ServerOnConnect, proc { connected = true })
179
179
  loop.attach server
180
- s = TCPSocket.open("localhost", port)
180
+ s = TCPSocket.open(HOST, port)
181
181
  loop.run_once
182
182
  s.close
183
183
  expect(connected).to eq true
@@ -201,9 +201,9 @@ describe Coolio::TCPServer do
201
201
  it "closed socket called on_close" do
202
202
  begin
203
203
  closed = false
204
- server = Cool.io::TCPServer.new("localhost", port, ServerOnConnect, proc { closed = true })
204
+ server = Cool.io::TCPServer.new(HOST, port, ServerOnConnect, proc { closed = true })
205
205
  loop.attach server
206
- s = TCPSocket.open("localhost", port)
206
+ s = TCPSocket.open(HOST, port)
207
207
  loop.run_once
208
208
  s.close
209
209
  loop.run_once
@@ -229,10 +229,10 @@ describe Coolio::TCPServer do
229
229
  it "server socket received data" do
230
230
  begin
231
231
  data = "aaa"
232
- 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 })
233
233
  loop.attach server
234
234
  thread = Thread.new { loop.run }
235
- s = TCPSocket.open("localhost", port)
235
+ s = TCPSocket.open(HOST, port)
236
236
  s.write "zzz"
237
237
  sleep 0.1
238
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