Skip to content

Commit afd7846

Browse files
committed
WIP Add spec for IO::Buffer.map
1 parent ffc54df commit afd7846

File tree

2 files changed

+728
-0
lines changed

2 files changed

+728
-0
lines changed
Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
require_relative '../../../spec_helper'
2+
3+
describe "IO::Buffer.map" do
4+
after :each do
5+
@buffer&.free
6+
@buffer = nil
7+
@file&.close
8+
@file = nil
9+
end
10+
11+
def open_fixture
12+
File.open("#{__dir__}/../fixtures/read_text.txt", "r+")
13+
end
14+
15+
it "creates a new buffer mapped from a file" do
16+
@file = open_fixture
17+
@buffer = IO::Buffer.map(@file)
18+
19+
@buffer.size.should == 9
20+
@buffer.get_string.should == "abcâdef\n".b
21+
end
22+
23+
it "allows to close the file after creating buffer, retaining mapping" do
24+
file = open_fixture
25+
@buffer = IO::Buffer.map(file)
26+
file.close
27+
28+
@buffer.get_string.should == "abcâdef\n".b
29+
end
30+
31+
ruby_version_is ""..."3.3" do
32+
it "creates a buffer with default state and expected flags" do
33+
@file = open_fixture
34+
@buffer = IO::Buffer.map(@file)
35+
36+
@buffer.should_not.internal?
37+
@buffer.should.mapped?
38+
@buffer.should.external?
39+
40+
@buffer.should_not.empty?
41+
@buffer.should_not.null?
42+
43+
@buffer.should.shared?
44+
@buffer.should_not.readonly?
45+
46+
@buffer.should_not.locked?
47+
@buffer.should.valid?
48+
end
49+
end
50+
51+
ruby_version_is "3.3" do
52+
it "creates a buffer with default state and expected flags" do
53+
@file = open_fixture
54+
@buffer = IO::Buffer.map(@file)
55+
56+
@buffer.should_not.internal?
57+
@buffer.should.mapped?
58+
@buffer.should.external?
59+
60+
@buffer.should_not.empty?
61+
@buffer.should_not.null?
62+
63+
@buffer.should.shared?
64+
@buffer.should_not.private?
65+
@buffer.should_not.readonly?
66+
67+
@buffer.should_not.locked?
68+
@buffer.should.valid?
69+
end
70+
end
71+
72+
platform_is_not :windows do
73+
it "is shareable across processes" do
74+
file_name = tmp("shared_buffer")
75+
@file = File.open(file_name, "w+")
76+
@file << "I'm private"
77+
@file.rewind
78+
@buffer = IO::Buffer.map(@file)
79+
80+
IO.popen("-") do |child_pipe|
81+
if child_pipe
82+
# Synchronize on child's output.
83+
child_pipe.readlines.first.chomp.should == @buffer.to_s
84+
@buffer.get_string.should == "I'm shared!"
85+
86+
@file.read.should == "I'm shared!"
87+
else
88+
@buffer.set_string("I'm shared!")
89+
puts @buffer
90+
end
91+
ensure
92+
child_pipe&.close
93+
end
94+
ensure
95+
File.unlink(file_name)
96+
end
97+
end
98+
99+
context "with an empty file" do
100+
ruby_version_is ""..."4.0" do
101+
it "raises a SystemCallError" do
102+
@file = File.open("#{__dir__}/../fixtures/empty.txt", "r+")
103+
-> { IO::Buffer.map(@file) }.should raise_error(SystemCallError)
104+
end
105+
end
106+
107+
ruby_version_is "4.0" do
108+
it "raises ArgumentError" do
109+
@file = File.open("#{__dir__}/../fixtures/empty.txt", "r+")
110+
-> { IO::Buffer.map(@file) }.should raise_error(ArgumentError, "Invalid negative or zero file size!")
111+
end
112+
end
113+
end
114+
115+
context "with a file opened only for reading" do
116+
it "raises a SystemCallError if no flags are used" do
117+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r")
118+
-> { IO::Buffer.map(@file) }.should raise_error(SystemCallError)
119+
end
120+
end
121+
122+
context "with size argument" do
123+
it "limits the buffer to the specified size in bytes, starting from the start of the file" do
124+
@file = open_fixture
125+
@buffer = IO::Buffer.map(@file, 4)
126+
127+
@buffer.size.should == 4
128+
@buffer.get_string.should == "abc\xC3".b
129+
end
130+
131+
it "maps the whole file if size is nil" do
132+
@file = open_fixture
133+
@buffer = IO::Buffer.map(@file, nil)
134+
135+
@buffer.size.should == 9
136+
end
137+
138+
context "if size is 0" do
139+
ruby_version_is ""..."4.0" do
140+
platform_is_not :windows do
141+
it "raises a SystemCallError" do
142+
@file = open_fixture
143+
-> { IO::Buffer.map(@file, 0) }.should raise_error(SystemCallError)
144+
end
145+
end
146+
end
147+
148+
ruby_version_is "4.0" do
149+
it "raises ArgumentError" do
150+
@file = open_fixture
151+
-> { IO::Buffer.map(@file, 0) }.should raise_error(ArgumentError, "Size can't be zero!")
152+
end
153+
end
154+
end
155+
156+
it "raises TypeError if size is not an Integer or nil" do
157+
@file = open_fixture
158+
-> { IO::Buffer.map(@file, "10") }.should raise_error(TypeError, "not an Integer")
159+
-> { IO::Buffer.map(@file, 10.0) }.should raise_error(TypeError, "not an Integer")
160+
end
161+
162+
it "raises ArgumentError if size is negative" do
163+
@file = open_fixture
164+
-> { IO::Buffer.map(@file, -1) }.should raise_error(ArgumentError, "Size can't be negative!")
165+
end
166+
167+
ruby_version_is ""..."4.0" do
168+
# May or may not cause a crash on access.
169+
it "is undefined behavior if size is larger than file size"
170+
end
171+
172+
ruby_version_is "4.0" do
173+
it "raises ArgumentError if size is larger than file size" do
174+
@file = open_fixture
175+
-> { IO::Buffer.map(@file, 8192) }.should raise_error(ArgumentError, "Size can't be larger than file size!")
176+
end
177+
end
178+
end
179+
180+
context "with size and offset arguments" do
181+
# Neither Windows nor macOS have clear, stable behavior with non-zero offset.
182+
# https://bugs.ruby-lang.org/issues/21700
183+
platform_is :linux do
184+
context "if offset is an allowed value for system call" do
185+
it "maps the span specified by size starting from the offset" do
186+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r+")
187+
@buffer = IO::Buffer.map(@file, 14, IO::Buffer::PAGE_SIZE)
188+
189+
@buffer.size.should == 14
190+
@buffer.get_string(0, 14).should == "rror if size i"
191+
end
192+
193+
context "if size is nil" do
194+
ruby_version_is ""..."4.0" do
195+
it "maps the rest of the file" do
196+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r+")
197+
@buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
198+
199+
@buffer.get_string(0, 1).should == "r"
200+
end
201+
202+
it "incorrectly sets buffer's size to file's full size" do
203+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r+")
204+
@buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
205+
206+
@buffer.size.should == @file.size
207+
end
208+
end
209+
210+
ruby_version_is "4.0" do
211+
it "maps the rest of the file" do
212+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r+")
213+
@buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
214+
215+
@buffer.get_string(0, 1).should == "r"
216+
end
217+
218+
it "sets buffer's size to file's remaining size" do
219+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r+")
220+
@buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
221+
222+
@buffer.size.should == (@file.size - IO::Buffer::PAGE_SIZE)
223+
end
224+
end
225+
end
226+
end
227+
end
228+
229+
it "maps the file from the start if offset is 0" do
230+
@file = open_fixture
231+
@buffer = IO::Buffer.map(@file, 4, 0)
232+
233+
@buffer.size.should == 4
234+
@buffer.get_string.should == "abc\xC3".b
235+
end
236+
237+
ruby_version_is ""..."4.0" do
238+
# May or may not cause a crash on access.
239+
it "is undefined behavior if offset+size is larger than file size"
240+
end
241+
242+
ruby_version_is "4.0" do
243+
it "raises ArgumentError if offset+size is larger than file size" do
244+
@file = File.open(fixture(__FILE__, "big_file.txt"), "r+")
245+
-> { IO::Buffer.map(@file, 8192, IO::Buffer::PAGE_SIZE) }.should raise_error(ArgumentError, "Offset too large!")
246+
end
247+
end
248+
249+
it "raises TypeError if offset is not convertible to Integer" do
250+
@file = open_fixture
251+
-> { IO::Buffer.map(@file, 4, "4096") }.should raise_error(TypeError, /no implicit conversion/)
252+
-> { IO::Buffer.map(@file, 4, nil) }.should raise_error(TypeError, /no implicit conversion/)
253+
end
254+
255+
it "raises a SystemCallError if offset is not an allowed value" do
256+
@file = open_fixture
257+
-> { IO::Buffer.map(@file, 4, 3) }.should raise_error(SystemCallError)
258+
end
259+
260+
ruby_version_is ""..."4.0" do
261+
it "raises a SystemCallError if offset is negative" do
262+
@file = open_fixture
263+
-> { IO::Buffer.map(@file, 4, -1) }.should raise_error(SystemCallError)
264+
end
265+
end
266+
267+
ruby_version_is "4.0" do
268+
it "raises ArgumentError if offset is negative" do
269+
@file = open_fixture
270+
-> { IO::Buffer.map(@file, 4, -1) }.should raise_error(ArgumentError, "Offset can't be negative!")
271+
end
272+
end
273+
end
274+
275+
context "with flags argument" do
276+
context "when READONLY flag is specified" do
277+
it "sets readonly flag on the buffer, allowing only reads" do
278+
@file = open_fixture
279+
@buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY)
280+
281+
@buffer.should.readonly?
282+
283+
@buffer.get_string.should == "abc\xC3\xA2def\n".b
284+
end
285+
286+
it "allows mapping read-only files" do
287+
@file = File.open("#{__dir__}/../fixtures/read_text.txt", "r")
288+
@buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY)
289+
290+
@buffer.should.readonly?
291+
292+
@buffer.get_string.should == "abc\xC3\xA2def\n".b
293+
end
294+
295+
it "causes IO::Buffer::AccessError on write" do
296+
@file = open_fixture
297+
@buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY)
298+
299+
-> { @buffer.set_string("test") }.should raise_error(IO::Buffer::AccessError, "Buffer is not writable!")
300+
end
301+
end
302+
303+
ruby_version_is "3.3" do
304+
context "when PRIVATE is specified" do
305+
it "sets private flag on the buffer, making it freely modifiable" do
306+
@file = open_fixture
307+
@buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE)
308+
309+
@buffer.should.private?
310+
@buffer.should_not.shared?
311+
@buffer.should_not.external?
312+
313+
@buffer.get_string.should == "abc\xC3\xA2def\n".b
314+
@buffer.set_string("test12345")
315+
@buffer.get_string.should == "test12345".b
316+
317+
@file.read.should == "abcâdef\n"
318+
end
319+
320+
it "allows mapping read-only files and modifying the buffer" do
321+
@file = File.open("#{__dir__}/../fixtures/read_text.txt", "r")
322+
@buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE)
323+
324+
@buffer.should.private?
325+
@buffer.should_not.shared?
326+
@buffer.should_not.external?
327+
328+
@buffer.get_string.should == "abc\xC3\xA2def\n".b
329+
@buffer.set_string("test12345")
330+
@buffer.get_string.should == "test12345".b
331+
332+
@file.read.should == "abcâdef\n"
333+
end
334+
335+
platform_is_not :windows do
336+
it "is not shared across processes" do
337+
file_name = tmp("shared_buffer")
338+
@file = File.open(file_name, "w+")
339+
@file << "I'm private"
340+
@file.rewind
341+
@buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE)
342+
343+
IO.popen("-") do |child_pipe|
344+
if child_pipe
345+
# Synchronize on child's output.
346+
child_pipe.readlines.first.chomp.should == @buffer.to_s
347+
@buffer.get_string.should == "I'm private"
348+
349+
@file.read.should == "I'm private"
350+
else
351+
@buffer.set_string("I'm shared!")
352+
puts @buffer
353+
end
354+
ensure
355+
child_pipe&.close
356+
end
357+
ensure
358+
File.unlink(file_name)
359+
end
360+
end
361+
end
362+
end
363+
end
364+
end

0 commit comments

Comments
 (0)