diff --git a/core/io/buffer/external_spec.rb b/core/io/buffer/external_spec.rb index 4377a38357..10bb51053d 100644 --- a/core/io/buffer/external_spec.rb +++ b/core/io/buffer/external_spec.rb @@ -6,103 +6,18 @@ @buffer = nil end - context "with a buffer created with .new" do - it "is false for an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.external?.should be_false - end - - it "is false for a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.external?.should be_false - end - end - - context "with a file-backed buffer created with .map" do - it "is true for a regular mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.external?.should be_true - end - end - - ruby_version_is "3.3" do - it "is false for a private mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.external?.should be_false - end - end - end - end - - context "with a String-backed buffer created with .for" do - it "is true for a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.external?.should be_true - end - - it "is true for a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.external?.should be_true - end - end + it "is true for a buffer with externally-managed memory" do + @buffer = IO::Buffer.for("string") + @buffer.external?.should be_true end - ruby_version_is "3.3" do - context "with a String-backed buffer created with .string" do - it "is true" do - IO::Buffer.string(4) do |buffer| - buffer.external?.should be_true - end - end - end + it "is false for a buffer with self-managed memory" do + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED) + @buffer.external?.should be_false end - # Always false for slices - context "with a slice of a buffer" do - context "created with .new" do - it "is false when slicing an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.slice.external?.should be_false - end - - it "is false when slicing a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.slice.external?.should be_false - end - end - - context "created with .map" do - it "is false" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.slice.external?.should be_false - end - end - end - - context "created with .for" do - it "is false when slicing a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.slice.external?.should be_false - end - - it "is false when slicing a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.slice.external?.should be_false - end - end - end - - ruby_version_is "3.3" do - context "created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.slice.external?.should be_false - end - end - end - end + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.external?.should be_false end end diff --git a/core/io/buffer/fixtures/big_file.txt b/core/io/buffer/fixtures/big_file.txt new file mode 100644 index 0000000000..e3495b4895 --- /dev/null +++ b/core/io/buffer/fixtures/big_file.txt @@ -0,0 +1,364 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer.map" do + after :each do + @buffer&.free + @buffer = nil + @file&.close + @file = nil + end + + def open_fixture + File.open("#{__dir__}/../fixtures/read_text.txt", "r+") + end + + it "creates a new buffer mapped from a file" do + @file = open_fixture + @buffer = IO::Buffer.map(@file) + + @buffer.size.should == 9 + @buffer.get_string.should == "abcâdef\n".b + end + + it "allows to close the file after creating buffer, retaining mapping" do + file = open_fixture + @buffer = IO::Buffer.map(file) + file.close + + @buffer.get_string.should == "abcâdef\n".b + end + + ruby_version_is ""..."3.3" do + it "creates a buffer with default state and expected flags" do + @file = open_fixture + @buffer = IO::Buffer.map(@file) + + @buffer.should_not.internal? + @buffer.should.mapped? + @buffer.should.external? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should.shared? + @buffer.should_not.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + + ruby_version_is "3.3" do + it "creates a buffer with default state and expected flags" do + @file = open_fixture + @buffer = IO::Buffer.map(@file) + + @buffer.should_not.internal? + @buffer.should.mapped? + @buffer.should.external? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should.shared? + @buffer.should_not.private? + @buffer.should_not.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + + platform_is_not :windows do + it "is shareable across processes" do + file_name = tmp("shared_buffer") + @file = File.open(file_name, "w+") + @file << "I'm private" + @file.rewind + @buffer = IO::Buffer.map(@file) + + IO.popen("-") do |child_pipe| + if child_pipe + # Synchronize on child's output. + child_pipe.readlines.first.chomp.should == @buffer.to_s + @buffer.get_string.should == "I'm shared!" + + @file.read.should == "I'm shared!" + else + @buffer.set_string("I'm shared!") + puts @buffer + end + ensure + child_pipe&.close + end + ensure + File.unlink(file_name) + end + end + + context "with an empty file" do + ruby_version_is ""..."4.0" do + it "raises a SystemCallError" do + @file = File.open("#{__dir__}/../fixtures/empty.txt", "r+") + -> { IO::Buffer.map(@file) }.should raise_error(SystemCallError) + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError" do + @file = File.open("#{__dir__}/../fixtures/empty.txt", "r+") + -> { IO::Buffer.map(@file) }.should raise_error(ArgumentError, "Invalid negative or zero file size!") + end + end + end + + context "with a file opened only for reading" do + it "raises a SystemCallError if no flags are used" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r") + -> { IO::Buffer.map(@file) }.should raise_error(SystemCallError) + end + end + + context "with size argument" do + it "limits the buffer to the specified size in bytes, starting from the start of the file" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, 4) + + @buffer.size.should == 4 + @buffer.get_string.should == "abc\xC3".b + end + + it "maps the whole file if size is nil" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil) + + @buffer.size.should == 9 + end + + context "if size is 0" do + ruby_version_is ""..."4.0" do + platform_is_not :windows do + it "raises a SystemCallError" do + @file = open_fixture + -> { IO::Buffer.map(@file, 0) }.should raise_error(SystemCallError) + end + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError" do + @file = open_fixture + -> { IO::Buffer.map(@file, 0) }.should raise_error(ArgumentError, "Size can't be zero!") + end + end + end + + it "raises TypeError if size is not an Integer or nil" do + @file = open_fixture + -> { IO::Buffer.map(@file, "10") }.should raise_error(TypeError, "not an Integer") + -> { IO::Buffer.map(@file, 10.0) }.should raise_error(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, -1) }.should raise_error(ArgumentError, "Size can't be negative!") + end + + ruby_version_is ""..."4.0" do + # May or may not cause a crash on access. + it "is undefined behavior if size is larger than file size" + end + + ruby_version_is "4.0" do + it "raises ArgumentError if size is larger than file size" do + @file = open_fixture + -> { IO::Buffer.map(@file, 8192) }.should raise_error(ArgumentError, "Size can't be larger than file size!") + end + end + end + + context "with size and offset arguments" do + # Neither Windows nor macOS have clear, stable behavior with non-zero offset. + # https://bugs.ruby-lang.org/issues/21700 + platform_is :linux do + context "if offset is an allowed value for system call" do + it "maps the span specified by size starting from the offset" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, 14, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == 14 + @buffer.get_string(0, 14).should == "rror if size i" + end + + context "if size is nil" do + ruby_version_is ""..."4.0" do + it "maps the rest of the file" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.get_string(0, 1).should == "r" + end + + it "incorrectly sets buffer's size to file's full size" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == @file.size + end + end + + ruby_version_is "4.0" do + it "maps the rest of the file" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.get_string(0, 1).should == "r" + end + + it "sets buffer's size to file's remaining size" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == (@file.size - IO::Buffer::PAGE_SIZE) + end + end + end + end + end + + it "maps the file from the start if offset is 0" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, 4, 0) + + @buffer.size.should == 4 + @buffer.get_string.should == "abc\xC3".b + end + + ruby_version_is ""..."4.0" do + # May or may not cause a crash on access. + it "is undefined behavior if offset+size is larger than file size" + end + + ruby_version_is "4.0" do + it "raises ArgumentError if offset+size is larger than file size" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + -> { IO::Buffer.map(@file, 8192, IO::Buffer::PAGE_SIZE) }.should raise_error(ArgumentError, "Offset too large!") + end + end + + it "raises TypeError if offset is not convertible to Integer" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, "4096") }.should raise_error(TypeError, /no implicit conversion/) + -> { IO::Buffer.map(@file, 4, nil) }.should raise_error(TypeError, /no implicit conversion/) + end + + it "raises a SystemCallError if offset is not an allowed value" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, 3) }.should raise_error(SystemCallError) + end + + ruby_version_is ""..."4.0" do + it "raises a SystemCallError if offset is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, -1) }.should raise_error(SystemCallError) + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError if offset is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, -1) }.should raise_error(ArgumentError, "Offset can't be negative!") + end + end + end + + context "with flags argument" do + context "when READONLY flag is specified" do + it "sets readonly flag on the buffer, allowing only reads" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY) + + @buffer.should.readonly? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + end + + it "allows mapping read-only files" do + @file = File.open("#{__dir__}/../fixtures/read_text.txt", "r") + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY) + + @buffer.should.readonly? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + end + + it "causes IO::Buffer::AccessError on write" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY) + + -> { @buffer.set_string("test") }.should raise_error(IO::Buffer::AccessError, "Buffer is not writable!") + end + end + + ruby_version_is "3.3" do + context "when PRIVATE is specified" do + it "sets private flag on the buffer, making it freely modifiable" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE) + + @buffer.should.private? + @buffer.should_not.shared? + @buffer.should_not.external? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + @buffer.set_string("test12345") + @buffer.get_string.should == "test12345".b + + @file.read.should == "abcâdef\n" + end + + it "allows mapping read-only files and modifying the buffer" do + @file = File.open("#{__dir__}/../fixtures/read_text.txt", "r") + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE) + + @buffer.should.private? + @buffer.should_not.shared? + @buffer.should_not.external? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + @buffer.set_string("test12345") + @buffer.get_string.should == "test12345".b + + @file.read.should == "abcâdef\n" + end + + platform_is_not :windows do + it "is not shared across processes" do + file_name = tmp("shared_buffer") + @file = File.open(file_name, "w+") + @file << "I'm private" + @file.rewind + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE) + + IO.popen("-") do |child_pipe| + if child_pipe + # Synchronize on child's output. + child_pipe.readlines.first.chomp.should == @buffer.to_s + @buffer.get_string.should == "I'm private" + + @file.read.should == "I'm private" + else + @buffer.set_string("I'm shared!") + puts @buffer + end + ensure + child_pipe&.close + end + ensure + File.unlink(file_name) + end + end + end + end + end +end diff --git a/core/io/buffer/for_spec.rb b/core/io/buffer/for_spec.rb new file mode 100644 index 0000000000..db299ef10a --- /dev/null +++ b/core/io/buffer/for_spec.rb @@ -0,0 +1,115 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer.for" do + before :each do + @string = +"för striñg" + end + + after :each do + @buffer&.free + @buffer = nil + end + + context "without a block" do + it "copies string's contents, creating a separate read-only buffer" do + @buffer = IO::Buffer.for(@string) + + @buffer.size.should == @string.bytesize + @buffer.get_string.should == @string.b + + @string[0] = "d" + @buffer.get_string(0, 1).should == "f".b + + -> { @buffer.set_string("d") }.should raise_error(IO::Buffer::AccessError, "Buffer is not writable!") + end + + ruby_version_is ""..."3.3" do + it "creates an external, read-only buffer" do + @buffer = IO::Buffer.for(@string) + + @buffer.should_not.internal? + @buffer.should_not.mapped? + @buffer.should.external? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should_not.shared? + @buffer.should.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + + ruby_version_is "3.3" do + it "creates an external, read-only buffer" do + @buffer = IO::Buffer.for(@string) + + @buffer.should_not.internal? + @buffer.should_not.mapped? + @buffer.should.external? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should_not.shared? + @buffer.should_not.private? + @buffer.should.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + end + + context "with a block" do + it "returns the last value in the block" do + value = + IO::Buffer.for(@string) do |buffer| + buffer.size * 3 + end + value.should == @string.bytesize * 3 + end + + it "frees the buffer at the end of the block" do + IO::Buffer.for(@string) do |buffer| + @buffer = buffer + @buffer.should_not.null? + end + @buffer.should.null? + end + + context "if string is not frozen" do + it "creates a modifiable string-backed buffer" do + IO::Buffer.for(@string) do |buffer| + buffer.size.should == @string.bytesize + buffer.get_string.should == @string.b + + buffer.should_not.readonly? + + buffer.set_string("ghost shell") + @string.should == "ghost shellg" + end + end + + it "locks the original string to prevent modification" do + IO::Buffer.for(@string) do |_buffer| + -> { @string[0] = "t" }.should raise_error(RuntimeError, "can't modify string; temporarily locked") + end + @string[1] = "u" + @string.should == "fur striñg" + end + end + + context "if string is frozen" do + it "creates a read-only string-backed buffer" do + IO::Buffer.for(@string.freeze) do |buffer| + buffer.should.readonly? + + -> { buffer.set_string("ghost shell") }.should raise_error(IO::Buffer::AccessError, "Buffer is not writable!") + end + end + end + end +end diff --git a/core/io/buffer/initialize_spec.rb b/core/io/buffer/initialize_spec.rb index c86d1e7f1d..adaa57b2e6 100644 --- a/core/io/buffer/initialize_spec.rb +++ b/core/io/buffer/initialize_spec.rb @@ -12,16 +12,37 @@ @buffer.each(:U8).should.all? { |_offset, value| value.eql?(0) } end - it "creates a buffer with default state" do - @buffer = IO::Buffer.new - @buffer.should_not.shared? - @buffer.should_not.readonly? + ruby_version_is ""..."3.3" do + it "creates a buffer with default state" do + @buffer = IO::Buffer.new + + @buffer.should_not.shared? + @buffer.should_not.readonly? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + + ruby_version_is "3.3" do + it "creates a buffer with default state" do + @buffer = IO::Buffer.new - @buffer.should_not.empty? - @buffer.should_not.null? + @buffer.should_not.external? - # This is run-time state, set by #locked. - @buffer.should_not.locked? + @buffer.should_not.shared? + @buffer.should_not.private? + @buffer.should_not.readonly? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should_not.locked? + @buffer.should.valid? + end end context "with size argument" do @@ -29,25 +50,24 @@ size = IO::Buffer::PAGE_SIZE - 1 @buffer = IO::Buffer.new(size) @buffer.size.should == size + @buffer.should_not.empty? + @buffer.should.internal? @buffer.should_not.mapped? - @buffer.should_not.empty? end it "creates a new mapped buffer if size is greater than or equal to IO::Buffer::PAGE_SIZE" do size = IO::Buffer::PAGE_SIZE @buffer = IO::Buffer.new(size) @buffer.size.should == size + @buffer.should_not.empty? + @buffer.should_not.internal? @buffer.should.mapped? - @buffer.should_not.empty? end it "creates a null buffer if size is 0" do @buffer = IO::Buffer.new(0) - @buffer.size.should.zero? - @buffer.should_not.internal? - @buffer.should_not.mapped? @buffer.should.null? @buffer.should.empty? end @@ -77,6 +97,29 @@ @buffer.should_not.empty? end + it "allows extra flags" do + @buffer = IO::Buffer.new(10, IO::Buffer::INTERNAL | IO::Buffer::SHARED | IO::Buffer::READONLY) + @buffer.should.internal? + @buffer.should.shared? + @buffer.should.readonly? + end + + it "ignores flags if size is 0" do + @buffer = IO::Buffer.new(0, 0xffff) + @buffer.should.null? + @buffer.should.empty? + + @buffer.should_not.internal? + @buffer.should_not.mapped? + @buffer.should_not.external? + + @buffer.should_not.shared? + @buffer.should_not.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + it "raises IO::Buffer::AllocationError if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do -> { IO::Buffer.new(10, IO::Buffer::READONLY) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") -> { IO::Buffer.new(10, 0) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") diff --git a/core/io/buffer/internal_spec.rb b/core/io/buffer/internal_spec.rb index 409699cc3c..40dc633d5d 100644 --- a/core/io/buffer/internal_spec.rb +++ b/core/io/buffer/internal_spec.rb @@ -6,103 +6,18 @@ @buffer = nil end - context "with a buffer created with .new" do - it "is true for an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.internal?.should be_true - end - - it "is false for a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.internal?.should be_false - end - end - - context "with a file-backed buffer created with .map" do - it "is false for a regular mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.internal?.should be_false - end - end - - ruby_version_is "3.3" do - it "is false for a private mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.internal?.should be_false - end - end - end - end - - context "with a String-backed buffer created with .for" do - it "is false for a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.internal?.should be_false - end - - it "is false for a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.internal?.should be_false - end - end + it "is true for an internally-allocated buffer" do + @buffer = IO::Buffer.new(12) + @buffer.internal?.should be_true end - ruby_version_is "3.3" do - context "with a String-backed buffer created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.internal?.should be_false - end - end - end + it "is false for an externally-allocated buffer" do + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED) + @buffer.internal?.should be_false end - # Always false for slices - context "with a slice of a buffer" do - context "created with .new" do - it "is false when slicing an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.slice.internal?.should be_false - end - - it "is false when slicing a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.slice.internal?.should be_false - end - end - - context "created with .map" do - it "is false" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.slice.internal?.should be_false - end - end - end - - context "created with .for" do - it "is false when slicing a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.slice.internal?.should be_false - end - - it "is false when slicing a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.slice.internal?.should be_false - end - end - end - - ruby_version_is "3.3" do - context "created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.slice.internal?.should be_false - end - end - end - end + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.internal?.should be_false end end diff --git a/core/io/buffer/map_spec.rb b/core/io/buffer/map_spec.rb new file mode 100644 index 0000000000..483b02b92e --- /dev/null +++ b/core/io/buffer/map_spec.rb @@ -0,0 +1,364 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer.map" do + after :each do + @buffer&.free + @buffer = nil + @file&.close + @file = nil + end + + def open_fixture + File.open("#{__dir__}/../fixtures/read_text.txt", "r+") + end + + it "creates a new buffer mapped from a file" do + @file = open_fixture + @buffer = IO::Buffer.map(@file) + + @buffer.size.should == 9 + @buffer.get_string.should == "abcâdef\n".b + end + + it "allows to close the file after creating buffer, retaining mapping" do + file = open_fixture + @buffer = IO::Buffer.map(file) + file.close + + @buffer.get_string.should == "abcâdef\n".b + end + + ruby_version_is ""..."3.3" do + it "creates a mapped, external, shared buffer" do + @file = open_fixture + @buffer = IO::Buffer.map(@file) + + @buffer.should_not.internal? + @buffer.should.mapped? + @buffer.should.external? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should.shared? + @buffer.should_not.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + + ruby_version_is "3.3" do + it "creates a mapped, external, shared buffer" do + @file = open_fixture + @buffer = IO::Buffer.map(@file) + + @buffer.should_not.internal? + @buffer.should.mapped? + @buffer.should.external? + + @buffer.should_not.empty? + @buffer.should_not.null? + + @buffer.should.shared? + @buffer.should_not.private? + @buffer.should_not.readonly? + + @buffer.should_not.locked? + @buffer.should.valid? + end + end + + platform_is_not :windows do + it "is shareable across processes" do + file_name = tmp("shared_buffer") + @file = File.open(file_name, "w+") + @file << "I'm private" + @file.rewind + @buffer = IO::Buffer.map(@file) + + IO.popen("-") do |child_pipe| + if child_pipe + # Synchronize on child's output. + child_pipe.readlines.first.chomp.should == @buffer.to_s + @buffer.get_string.should == "I'm shared!" + + @file.read.should == "I'm shared!" + else + @buffer.set_string("I'm shared!") + puts @buffer + end + ensure + child_pipe&.close + end + ensure + File.unlink(file_name) + end + end + + context "with an empty file" do + ruby_version_is ""..."4.0" do + it "raises a SystemCallError" do + @file = File.open("#{__dir__}/../fixtures/empty.txt", "r+") + -> { IO::Buffer.map(@file) }.should raise_error(SystemCallError) + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError" do + @file = File.open("#{__dir__}/../fixtures/empty.txt", "r+") + -> { IO::Buffer.map(@file) }.should raise_error(ArgumentError, "Invalid negative or zero file size!") + end + end + end + + context "with a file opened only for reading" do + it "raises a SystemCallError if no flags are used" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r") + -> { IO::Buffer.map(@file) }.should raise_error(SystemCallError) + end + end + + context "with size argument" do + it "limits the buffer to the specified size in bytes, starting from the start of the file" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, 4) + + @buffer.size.should == 4 + @buffer.get_string.should == "abc\xC3".b + end + + it "maps the whole file if size is nil" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil) + + @buffer.size.should == 9 + end + + context "if size is 0" do + ruby_version_is ""..."4.0" do + platform_is_not :windows do + it "raises a SystemCallError" do + @file = open_fixture + -> { IO::Buffer.map(@file, 0) }.should raise_error(SystemCallError) + end + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError" do + @file = open_fixture + -> { IO::Buffer.map(@file, 0) }.should raise_error(ArgumentError, "Size can't be zero!") + end + end + end + + it "raises TypeError if size is not an Integer or nil" do + @file = open_fixture + -> { IO::Buffer.map(@file, "10") }.should raise_error(TypeError, "not an Integer") + -> { IO::Buffer.map(@file, 10.0) }.should raise_error(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, -1) }.should raise_error(ArgumentError, "Size can't be negative!") + end + + ruby_version_is ""..."4.0" do + # May or may not cause a crash on access. + it "is undefined behavior if size is larger than file size" + end + + ruby_version_is "4.0" do + it "raises ArgumentError if size is larger than file size" do + @file = open_fixture + -> { IO::Buffer.map(@file, 8192) }.should raise_error(ArgumentError, "Size can't be larger than file size!") + end + end + end + + context "with size and offset arguments" do + # Neither Windows nor macOS have clear, stable behavior with non-zero offset. + # https://bugs.ruby-lang.org/issues/21700 + platform_is :linux do + context "if offset is an allowed value for system call" do + it "maps the span specified by size starting from the offset" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, 14, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == 14 + @buffer.get_string(0, 14).should == "rror if size i" + end + + context "if size is nil" do + ruby_version_is ""..."4.0" do + it "maps the rest of the file" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.get_string(0, 1).should == "r" + end + + it "incorrectly sets buffer's size to file's full size" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == @file.size + end + end + + ruby_version_is "4.0" do + it "maps the rest of the file" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.get_string(0, 1).should == "r" + end + + it "sets buffer's size to file's remaining size" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == (@file.size - IO::Buffer::PAGE_SIZE) + end + end + end + end + end + + it "maps the file from the start if offset is 0" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, 4, 0) + + @buffer.size.should == 4 + @buffer.get_string.should == "abc\xC3".b + end + + ruby_version_is ""..."4.0" do + # May or may not cause a crash on access. + it "is undefined behavior if offset+size is larger than file size" + end + + ruby_version_is "4.0" do + it "raises ArgumentError if offset+size is larger than file size" do + @file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + -> { IO::Buffer.map(@file, 8192, IO::Buffer::PAGE_SIZE) }.should raise_error(ArgumentError, "Offset too large!") + end + end + + it "raises TypeError if offset is not convertible to Integer" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, "4096") }.should raise_error(TypeError, /no implicit conversion/) + -> { IO::Buffer.map(@file, 4, nil) }.should raise_error(TypeError, /no implicit conversion/) + end + + it "raises a SystemCallError if offset is not an allowed value" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, 3) }.should raise_error(SystemCallError) + end + + ruby_version_is ""..."4.0" do + it "raises a SystemCallError if offset is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, -1) }.should raise_error(SystemCallError) + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError if offset is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, -1) }.should raise_error(ArgumentError, "Offset can't be negative!") + end + end + end + + context "with flags argument" do + context "when READONLY flag is specified" do + it "sets readonly flag on the buffer, allowing only reads" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY) + + @buffer.should.readonly? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + end + + it "allows mapping read-only files" do + @file = File.open("#{__dir__}/../fixtures/read_text.txt", "r") + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY) + + @buffer.should.readonly? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + end + + it "causes IO::Buffer::AccessError on write" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY) + + -> { @buffer.set_string("test") }.should raise_error(IO::Buffer::AccessError, "Buffer is not writable!") + end + end + + ruby_version_is "3.3" do + context "when PRIVATE is specified" do + it "sets private flag on the buffer, making it freely modifiable" do + @file = open_fixture + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE) + + @buffer.should.private? + @buffer.should_not.shared? + @buffer.should_not.external? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + @buffer.set_string("test12345") + @buffer.get_string.should == "test12345".b + + @file.read.should == "abcâdef\n" + end + + it "allows mapping read-only files and modifying the buffer" do + @file = File.open("#{__dir__}/../fixtures/read_text.txt", "r") + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE) + + @buffer.should.private? + @buffer.should_not.shared? + @buffer.should_not.external? + + @buffer.get_string.should == "abc\xC3\xA2def\n".b + @buffer.set_string("test12345") + @buffer.get_string.should == "test12345".b + + @file.read.should == "abcâdef\n" + end + + platform_is_not :windows do + it "is not shared across processes" do + file_name = tmp("shared_buffer") + @file = File.open(file_name, "w+") + @file << "I'm private" + @file.rewind + @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE) + + IO.popen("-") do |child_pipe| + if child_pipe + # Synchronize on child's output. + child_pipe.readlines.first.chomp.should == @buffer.to_s + @buffer.get_string.should == "I'm private" + + @file.read.should == "I'm private" + else + @buffer.set_string("I'm shared!") + puts @buffer + end + ensure + child_pipe&.close + end + ensure + File.unlink(file_name) + end + end + end + end + end +end diff --git a/core/io/buffer/mapped_spec.rb b/core/io/buffer/mapped_spec.rb index b3610207ff..13dc548ed2 100644 --- a/core/io/buffer/mapped_spec.rb +++ b/core/io/buffer/mapped_spec.rb @@ -6,103 +6,18 @@ @buffer = nil end - context "with a buffer created with .new" do - it "is false for an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.mapped?.should be_false - end - - it "is true for a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.mapped?.should be_true - end - end - - context "with a file-backed buffer created with .map" do - it "is true for a regular mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.mapped?.should be_true - end - end - - ruby_version_is "3.3" do - it "is true for a private mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.mapped?.should be_true - end - end - end - end - - context "with a String-backed buffer created with .for" do - it "is false for a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.mapped?.should be_false - end - - it "is false for a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.mapped?.should be_false - end - end + it "is true for a buffer with mapped memory" do + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED) + @buffer.mapped?.should be_true end - ruby_version_is "3.3" do - context "with a String-backed buffer created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.mapped?.should be_false - end - end - end + it "is false for a buffer with non-mapped memory" do + @buffer = IO::Buffer.for("string") + @buffer.mapped?.should be_false end - # Always false for slices - context "with a slice of a buffer" do - context "created with .new" do - it "is false when slicing an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.slice.mapped?.should be_false - end - - it "is false when slicing a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.slice.mapped?.should be_false - end - end - - context "created with .map" do - it "is false" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.slice.mapped?.should be_false - end - end - end - - context "created with .for" do - it "is false when slicing a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.slice.mapped?.should be_false - end - - it "is false when slicing a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.slice.mapped?.should be_false - end - end - end - - ruby_version_is "3.3" do - context "created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.slice.mapped?.should be_false - end - end - end - end + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.mapped?.should be_false end end diff --git a/core/io/buffer/private_spec.rb b/core/io/buffer/private_spec.rb index 7aa308997b..c27cfc3958 100644 --- a/core/io/buffer/private_spec.rb +++ b/core/io/buffer/private_spec.rb @@ -7,105 +7,19 @@ @buffer = nil end - context "with a buffer created with .new" do - it "is false for an internal buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) - @buffer.private?.should be_false - end - - it "is false for a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.private?.should be_false - end - end - - context "with a file-backed buffer created with .map" do - it "is false for a regular mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.private?.should be_false - end - end - - it "is true for a private mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.private?.should be_true - end - end - end - - context "with a String-backed buffer created with .for" do - it "is false for a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.private?.should be_false - end - - it "is false for a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.private?.should be_false - end - end + it "is true for a buffer created with PRIVATE flag" do + @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::PRIVATE) + @buffer.private?.should be_true end - context "with a String-backed buffer created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.private?.should be_false - end - end + it "is false for a buffer created without PRIVATE flag" do + @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL) + @buffer.private?.should be_false end - # Always false for slices - context "with a slice of a buffer" do - context "created with .new" do - it "is false when slicing an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.slice.private?.should be_false - end - - it "is false when slicing a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.slice.private?.should be_false - end - end - - context "created with .map" do - it "is false when slicing a regular file-backed buffer" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.slice.private?.should be_false - end - end - - it "is false when slicing a private file-backed buffer" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.slice.private?.should be_false - end - end - end - - context "created with .for" do - it "is false when slicing a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.slice.private?.should be_false - end - - it "is false when slicing a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.slice.private?.should be_false - end - end - end - - context "created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.slice.private?.should be_false - end - end - end + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.private?.should be_false end end end diff --git a/core/io/buffer/readonly_spec.rb b/core/io/buffer/readonly_spec.rb index 0014a876ed..2fc7d340b7 100644 --- a/core/io/buffer/readonly_spec.rb +++ b/core/io/buffer/readonly_spec.rb @@ -6,138 +6,23 @@ @buffer = nil end - context "with a buffer created with .new" do - it "is false for an internal buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) - @buffer.readonly?.should be_false - end - - it "is false for a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.readonly?.should be_false - end - end - - context "with a file-backed buffer created with .map" do - it "is false for a writable mapping" do - File.open(__FILE__, "r+") do |file| - @buffer = IO::Buffer.map(file) - @buffer.readonly?.should be_false - end - end - - it "is true for a readonly mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.readonly?.should be_true - end - end - - ruby_version_is "3.3" do - it "is false for a private mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) - @buffer.readonly?.should be_false - end - end - end + it "is true for a buffer created with READONLY flag" do + @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::READONLY) + @buffer.readonly?.should be_true end - context "with a String-backed buffer created with .for" do - it "is true for a buffer created without a block" do - @buffer = IO::Buffer.for(+"test") - @buffer.readonly?.should be_true - end - - it "is false for a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.readonly?.should be_false - end - end - - it "is true for a buffer created with a block from a frozen string" do - IO::Buffer.for(-"test") do |buffer| - buffer.readonly?.should be_true - end - end + it "is true for a buffer that is non-writable" do + @buffer = IO::Buffer.for("string") + @buffer.readonly?.should be_true end - ruby_version_is "3.3" do - context "with a String-backed buffer created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.readonly?.should be_false - end - end - end + it "is false for a modifiable buffer" do + @buffer = IO::Buffer.new(12) + @buffer.readonly?.should be_false end - # This seems to be the only flag propagated from the source buffer to the slice. - context "with a slice of a buffer" do - context "created with .new" do - it "is false when slicing an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.slice.readonly?.should be_false - end - - it "is false when slicing a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.slice.readonly?.should be_false - end - end - - context "created with .map" do - it "is false when slicing a read-write file-backed buffer" do - File.open(__FILE__, "r+") do |file| - @buffer = IO::Buffer.map(file) - @buffer.slice.readonly?.should be_false - end - end - - it "is true when slicing a readonly file-backed buffer" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.slice.readonly?.should be_true - end - end - - ruby_version_is "3.3" do - it "is false when slicing a private file-backed buffer" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) - @buffer.slice.readonly?.should be_false - end - end - end - end - - context "created with .for" do - it "is true when slicing a buffer created without a block" do - @buffer = IO::Buffer.for(+"test") - @buffer.slice.readonly?.should be_true - end - - it "is false when slicing a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.slice.readonly?.should be_false - end - end - - it "is true when slicing a buffer created with a block from a frozen string" do - IO::Buffer.for(-"test") do |buffer| - buffer.slice.readonly?.should be_true - end - end - end - - ruby_version_is "3.3" do - context "created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.slice.readonly?.should be_false - end - end - end - end + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.readonly?.should be_false end end diff --git a/core/io/buffer/shared_spec.rb b/core/io/buffer/shared_spec.rb index f2a638cf39..ffcd4293f7 100644 --- a/core/io/buffer/shared_spec.rb +++ b/core/io/buffer/shared_spec.rb @@ -6,112 +6,25 @@ @buffer = nil end - context "with a buffer created with .new" do - it "is false for an internal buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) - @buffer.shared?.should be_false - end - - it "is false for a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.shared?.should be_false - end - end - - context "with a file-backed buffer created with .map" do - it "is true for a regular mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.shared?.should be_true - end - end - - ruby_version_is "3.3" do - it "is false for a private mapping" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.shared?.should be_false - end - end - end + it "is true for a buffer created with SHARED flag" do + @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::SHARED) + @buffer.shared?.should be_true end - context "with a String-backed buffer created with .for" do - it "is false for a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.shared?.should be_false - end - - it "is false for a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.shared?.should be_false - end - end + it "is true for a non-private buffer created with .map" do + file = File.open(fixture(__FILE__, "big_file.txt"), "r+") + @buffer = IO::Buffer.map(file) + file.close + @buffer.shared?.should be_true end - ruby_version_is "3.3" do - context "with a String-backed buffer created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.shared?.should be_false - end - end - end + it "is false for an unshared buffer" do + @buffer = IO::Buffer.new(12) + @buffer.shared?.should be_false end - # Always false for slices - context "with a slice of a buffer" do - context "created with .new" do - it "is false when slicing an internal buffer" do - @buffer = IO::Buffer.new(4) - @buffer.slice.shared?.should be_false - end - - it "is false when slicing a mapped buffer" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - @buffer.slice.shared?.should be_false - end - end - - context "created with .map" do - it "is false when slicing a regular file-backed buffer" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) - @buffer.slice.shared?.should be_false - end - end - - ruby_version_is "3.3" do - it "is false when slicing a private file-backed buffer" do - File.open(__FILE__, "r") do |file| - @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) - @buffer.slice.shared?.should be_false - end - end - end - end - - context "created with .for" do - it "is false when slicing a buffer created without a block" do - @buffer = IO::Buffer.for("test") - @buffer.slice.shared?.should be_false - end - - it "is false when slicing a buffer created with a block" do - IO::Buffer.for(+"test") do |buffer| - buffer.slice.shared?.should be_false - end - end - end - - ruby_version_is "3.3" do - context "created with .string" do - it "is false" do - IO::Buffer.string(4) do |buffer| - buffer.slice.shared?.should be_false - end - end - end - end + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.shared?.should be_false end end diff --git a/core/io/buffer/string_spec.rb b/core/io/buffer/string_spec.rb new file mode 100644 index 0000000000..b2786fdb9c --- /dev/null +++ b/core/io/buffer/string_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "IO::Buffer.string" do + it "creates a modifiable buffer for the duration of the block" do + IO::Buffer.string(7) do |buffer| + @buffer = buffer + + buffer.size.should == 7 + buffer.get_string.should == "\0\0\0\0\0\0\0".b + + buffer.set_string("test") + buffer.get_string.should == "test\0\0\0" + end + @buffer.should.null? + end + + it "returns contents of the buffer as a binary string" do + string = + IO::Buffer.string(7) do |buffer| + buffer.set_string("ä test") + end + string.should == "\xC3\xA4 test".b + end + + it "creates an external buffer" do + IO::Buffer.string(8) do |buffer| + buffer.should_not.internal? + buffer.should_not.mapped? + buffer.should.external? + + buffer.should_not.empty? + buffer.should_not.null? + + buffer.should_not.shared? + buffer.should_not.private? + buffer.should_not.readonly? + + buffer.should_not.locked? + buffer.should.valid? + end + end + + it "returns an empty string if size is 0" do + string = + IO::Buffer.string(0) do |buffer| + buffer.size.should == 0 + end + string.should == "" + end + + it "raises ArgumentError if size is negative" do + -> { IO::Buffer.string(-1) {} }.should raise_error(ArgumentError, "negative string size (or size too big)") + end + + it "raises RangeError if size is too large" do + -> { IO::Buffer.string(2 ** 232) {} }.should raise_error(RangeError, /\Abignum too big to convert into [`']long'\z/) + end + + it "raises LocalJumpError if no block is given" do + -> { IO::Buffer.string(7) }.should raise_error(LocalJumpError, "no block given") + end + end +end