diff options
Diffstat (limited to 'spec/ruby/core/io/buffer')
23 files changed, 1725 insertions, 0 deletions
diff --git a/spec/ruby/core/io/buffer/and_spec.rb b/spec/ruby/core/io/buffer/and_spec.rb new file mode 100644 index 0000000000..3cea163b0e --- /dev/null +++ b/spec/ruby/core/io/buffer/and_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../../spec_helper' + +describe :io_buffer_and, shared: true do + it "applies the argument buffer as an AND bit mask across the whole buffer" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer.send(@method, mask) + result.get_string.should == "\x30\x02\x30\x04\x30".b + result.free + end + end + end + + it "ignores extra parts of mask if it is longer than source buffer" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F\x00\x00\x00\xFF\xFF") do |mask| + result = buffer.send(@method, mask) + result.get_string.should == "\x30\x02\x00\x00\x00".b + result.free + end + end + end + + it "raises TypeError if mask is not an IO::Buffer" do + IO::Buffer.for(+"12345") do |buffer| + -> { buffer.send(@method, "\xF8\x8F") }.should.raise(TypeError, "wrong argument type String (expected IO::Buffer)") + -> { buffer.send(@method, 0xF8) }.should.raise(TypeError, "wrong argument type Integer (expected IO::Buffer)") + -> { buffer.send(@method, nil) }.should.raise(TypeError, "wrong argument type nil (expected IO::Buffer)") + end + end +end + +describe "IO::Buffer#&" do + it_behaves_like :io_buffer_and, :& + + it "creates a new internal buffer of the same size" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer & mask + result.should_not.equal? buffer + result.should.internal? + result.size.should == buffer.size + result.free + buffer.get_string.should == "12345".b + end + end + end +end + +describe "IO::Buffer#and!" do + it_behaves_like :io_buffer_and, :and! + + it "modifies the buffer in place" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer.and!(mask) + result.should.equal? buffer + result.should.external? + end + end + end +end diff --git a/spec/ruby/core/io/buffer/bit_count_spec.rb b/spec/ruby/core/io/buffer/bit_count_spec.rb new file mode 100644 index 0000000000..62f56f5b98 --- /dev/null +++ b/spec/ruby/core/io/buffer/bit_count_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../../spec_helper' + +ruby_version_is "4.1" do + describe "IO::Buffer#bit_count" do + it "counts all set bits in the whole buffer" do + IO::Buffer.for(+"\xFF\x00\x0F") do |buffer| + buffer.bit_count.should == 12 + end + end + + it "returns 0 for a buffer of all zero bytes" do + IO::Buffer.for(+"\x00\x00\x00") do |buffer| + buffer.bit_count.should == 0 + end + end + + it "returns 8 * size for a buffer of all 0xFF bytes" do + IO::Buffer.for(+"\xFF" * 9) do |buffer| + buffer.bit_count.should == 72 + end + end + + it "returns 0 for an empty buffer" do + IO::Buffer.new(0).bit_count.should == 0 + end + + it "accepts an offset to start counting from (length defaults to remaining bytes)" do + IO::Buffer.for(+"\xFF\x00\x0F") do |buffer| + buffer.bit_count(0).should == 12 # offset=0 => entire buffer + buffer.bit_count(1).should == 4 # offset=1 => 0x00 + 0x0F + buffer.bit_count(2).should == 4 # offset=2 => 0x0F only + end + end + + it "accepts an offset and length to restrict the counted region" do + IO::Buffer.for(+"\xFF\x00\x0F") do |buffer| + buffer.bit_count(0, 1).should == 8 # just 0xFF + buffer.bit_count(1, 1).should == 0 # just 0x00 + buffer.bit_count(2, 1).should == 4 # just 0x0F + buffer.bit_count(1, 2).should == 4 # 0x00 + 0x0F + end + end + + it "handles 8-byte aligned buffers efficiently" do + IO::Buffer.for(+"\xAA" * 8) do |buffer| + # 0xAA = 10101010 => 4 bits per byte => 32 total + buffer.bit_count.should == 32 + end + end + + it "raises ArgumentError when offset + length exceeds buffer size" do + IO::Buffer.for(+"\xFF") do |buffer| + -> { buffer.bit_count(0, 2) }.should.raise(ArgumentError) + -> { buffer.bit_count(1, 1) }.should.raise(ArgumentError) + end + end + + it "returns an Integer" do + IO::Buffer.for(+"\xFF") do |buffer| + buffer.bit_count.should.is_a?(Integer) + end + end + end +end diff --git a/spec/ruby/core/io/buffer/empty_spec.rb b/spec/ruby/core/io/buffer/empty_spec.rb new file mode 100644 index 0000000000..4cceb4dc0d --- /dev/null +++ b/spec/ruby/core/io/buffer/empty_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../../spec_helper' +require_relative 'shared/null_and_empty' + +describe "IO::Buffer#empty?" do + after :each do + @buffer&.free + @buffer = nil + end + + it_behaves_like :io_buffer_null_and_empty, :empty? + + it "is true for a 0-length String-backed buffer created with .for" do + @buffer = IO::Buffer.for("") + @buffer.empty?.should == true + end + + it "is true for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.empty?.should == true + end + end + + it "is true for a 0-length slice of a buffer with size > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(3, 0).empty?.should == true + end +end diff --git a/spec/ruby/core/io/buffer/external_spec.rb b/spec/ruby/core/io/buffer/external_spec.rb new file mode 100644 index 0000000000..18b2ee0d06 --- /dev/null +++ b/spec/ruby/core/io/buffer/external_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#external?" do + after :each do + @buffer&.free + @buffer = nil + end + + it "is true for a buffer with externally-managed memory" do + @buffer = IO::Buffer.for("string") + @buffer.external?.should == true + end + + it "is false for a buffer with self-managed memory" do + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED) + @buffer.external?.should == false + end + + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.external?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/for_spec.rb b/spec/ruby/core/io/buffer/for_spec.rb new file mode 100644 index 0000000000..7971ce0b71 --- /dev/null +++ b/spec/ruby/core/io/buffer/for_spec.rb @@ -0,0 +1,94 @@ +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(IO::Buffer::AccessError, "Buffer is not writable!") + end + + 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 + + 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(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(IO::Buffer::AccessError, "Buffer is not writable!") + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/free_spec.rb b/spec/ruby/core/io/buffer/free_spec.rb new file mode 100644 index 0000000000..20fb7b901f --- /dev/null +++ b/spec/ruby/core/io/buffer/free_spec.rb @@ -0,0 +1,102 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#free" do + context "with a buffer created with .new" do + it "frees internal memory and nullifies the buffer" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should == true + end + + it "frees mapped memory and nullifies the buffer" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + buffer.free + buffer.null?.should == true + end + end + + context "with a file-backed buffer created with .map" do + it "frees mapped memory and nullifies the buffer" do + File.open(__FILE__, "r") do |file| + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + buffer.free + buffer.null?.should == true + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = +"test" + buffer = IO::Buffer.for(string) + # Read-only buffer, can't modify the string. + buffer.free + buffer.null?.should == true + end + end + + context "with a block" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = +"test" + IO::Buffer.for(string) do |buffer| + buffer.set_string("meat") + buffer.free + buffer.null?.should == true + end + string.should == "meat" + end + end + end + + context "with a String-backed buffer created with .string" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = + IO::Buffer.string(4) do |buffer| + buffer.set_string("meat") + buffer.free + buffer.null?.should == true + end + string.should == "meat" + end + end + + it "can be called repeatedly without an error" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should == true + buffer.free + buffer.null?.should == true + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + buffer = IO::Buffer.new(4) + buffer.locked do + -> { buffer.free }.should.raise(IO::Buffer::LockedError, "Buffer is locked!") + end + buffer.free + buffer.null?.should == true + end + + context "with a slice of a buffer" do + it "nullifies the slice, not touching the buffer" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + + slice.free + slice.null?.should == true + buffer.null?.should == false + + buffer.free + end + + it "nullifies buffer, invalidating the slice" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + + buffer.free + slice.null?.should == false + slice.valid?.should == false + end + end +end diff --git a/spec/ruby/core/io/buffer/initialize_spec.rb b/spec/ruby/core/io/buffer/initialize_spec.rb new file mode 100644 index 0000000000..dba6fddc79 --- /dev/null +++ b/spec/ruby/core/io/buffer/initialize_spec.rb @@ -0,0 +1,119 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#initialize" do + after :each do + @buffer&.free + @buffer = nil + end + + it "creates a new zero-filled buffer with default size" do + @buffer = IO::Buffer.new + @buffer.size.should == IO::Buffer::DEFAULT_SIZE + @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.external? + + @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 + + context "with size argument" do + it "creates a new internal buffer if size is less than IO::Buffer::PAGE_SIZE" do + 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? + 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? + end + + it "creates a null buffer if size is 0" do + @buffer = IO::Buffer.new(0) + @buffer.should.null? + @buffer.should.empty? + end + + it "raises TypeError if size is not an Integer" do + -> { IO::Buffer.new(nil) }.should.raise(TypeError, "not an Integer") + -> { IO::Buffer.new(10.0) }.should.raise(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + -> { IO::Buffer.new(-1) }.should.raise(ArgumentError, "Size can't be negative!") + end + end + + context "with size and flags arguments" do + it "forces mapped buffer with IO::Buffer::MAPPED flag" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE - 1, IO::Buffer::MAPPED) + @buffer.should.mapped? + @buffer.should_not.internal? + @buffer.should_not.empty? + end + + it "forces internal buffer with IO::Buffer::INTERNAL flag" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::INTERNAL) + @buffer.should.internal? + @buffer.should_not.mapped? + @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(IO::Buffer::AllocationError, "Could not allocate buffer!") + -> { IO::Buffer.new(10, 0) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + + it "raises ArgumentError if flags is negative" do + -> { IO::Buffer.new(10, -1) }.should.raise(ArgumentError, "Flags can't be negative!") + end + + it "raises TypeError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should.raise(TypeError, "not an Integer") + end + end +end diff --git a/spec/ruby/core/io/buffer/internal_spec.rb b/spec/ruby/core/io/buffer/internal_spec.rb new file mode 100644 index 0000000000..73e19eb85e --- /dev/null +++ b/spec/ruby/core/io/buffer/internal_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#internal?" do + after :each do + @buffer&.free + @buffer = nil + end + + it "is true for an internally-allocated buffer" do + @buffer = IO::Buffer.new(12) + @buffer.internal?.should == true + end + + it "is false for an externally-allocated buffer" do + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED) + @buffer.internal?.should == false + end + + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.internal?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/locked_spec.rb b/spec/ruby/core/io/buffer/locked_spec.rb new file mode 100644 index 0000000000..249026aa8a --- /dev/null +++ b/spec/ruby/core/io/buffer/locked_spec.rb @@ -0,0 +1,75 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#locked" do + after :each do + @buffer&.free + @buffer = nil + end + + context "when buffer is locked" do + it "allows reading and writing operations on the buffer" do + @buffer = IO::Buffer.new(4) + @buffer.set_string("test") + @buffer.locked do + @buffer.get_string.should == "test" + @buffer.set_string("meat") + end + @buffer.get_string.should == "meat" + end + + it "disallows operations changing buffer itself, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + # Just an example, each method is responsible for checking the lock state. + -> { @buffer.resize(8) }.should.raise(IO::Buffer::LockedError) + end + end + end + + it "disallows reentrant locking, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.locked {} }.should.raise(IO::Buffer::LockedError, "Buffer already locked!") + end + end + + it "does not propagate to buffer's slices" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.locked do + @buffer.locked?.should == true + slice.locked?.should == false + slice.locked { slice.locked?.should == true } + end + end + + it "does not propagate backwards from buffer's slices" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + slice.locked do + slice.locked?.should == true + @buffer.locked?.should == false + @buffer.locked { @buffer.locked?.should == true } + end + end +end + +describe "IO::Buffer#locked?" do + after :each do + @buffer&.free + @buffer = nil + end + + it "is false by default" do + @buffer = IO::Buffer.new(4) + @buffer.locked?.should == false + end + + it "is true only inside of #locked block" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + @buffer.locked?.should == true + end + @buffer.locked?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/map_spec.rb b/spec/ruby/core/io/buffer/map_spec.rb new file mode 100644 index 0000000000..23e837ce07 --- /dev/null +++ b/spec/ruby/core/io/buffer/map_spec.rb @@ -0,0 +1,343 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer.map" do + before :all do + @tmp_files = [] + + @big_file_name = nil + @small_file_name = nil + end + + after :all do + @tmp_files.each {|file| File.delete(file)} + end + + def open_fixture + unless @small_file_name + @small_file_name = tmp("read_text.txt") + File.copy_stream(fixture(__dir__, "read_text.txt"), @small_file_name) + @tmp_files << @small_file_name + end + File.open(@small_file_name, "rb+") + end + + def open_big_file_fixture + unless @big_file_name + @big_file_name = tmp("big_file") + # Usually 4 kibibytes + 16 bytes + File.write(@big_file_name, "12345678" * (IO::Buffer::PAGE_SIZE / 8 + 2)) + @tmp_files << @big_file_name + end + File.open(@big_file_name, "rb+") + end + + after :each do + @buffer&.free + @buffer = nil + @file&.close + @file = nil + 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 + + 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 + + platform_is_not :windows, :openbsd 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 ArgumentError" do + file_name = tmp("empty.txt") + @file = File.open(file_name, "wb+") + @tmp_files << file_name + -> { IO::Buffer.map(@file) }.should.raise(ArgumentError, "Invalid negative or zero file size!") + end + end + end + + context "with a file opened only for reading" do + it "raises a SystemCallError unless read-only" do + @file = File.open(fixture(__dir__, "read_text.txt"), "rb") + -> { IO::Buffer.map(@file) }.should.raise(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 + it "raises ArgumentError" do + @file = open_fixture + -> { IO::Buffer.map(@file, 0) }.should.raise(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(TypeError, "not an Integer") + -> { IO::Buffer.map(@file, 10.0) }.should.raise(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + @file = open_fixture + -> { IO::Buffer.map(@file, -1) }.should.raise(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(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 = open_big_file_fixture + @buffer = IO::Buffer.map(@file, 14, IO::Buffer::PAGE_SIZE) + + @buffer.size.should == 14 + @buffer.get_string(0, 14).should == "12345678123456" + end + + context "if size is nil" do + ruby_version_is ""..."4.0" do + it "maps the rest of the file" do + @file = open_big_file_fixture + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.get_string(0, 1).should == "1" + end + + it "incorrectly sets buffer's size to file's full size" do + @file = open_big_file_fixture + @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 = open_big_file_fixture + @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE) + + @buffer.get_string(0, 1).should == "1" + end + + it "sets buffer's size to file's remaining size" do + @file = open_big_file_fixture + @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 = open_big_file_fixture + -> { IO::Buffer.map(@file, 17, IO::Buffer::PAGE_SIZE) }.should.raise(ArgumentError, "Offset too large!") + ensure + # Windows requires the file to be closed before deletion. + @file.close unless @file.closed? + end + end + + it "raises TypeError if offset is not convertible to Integer" do + @file = open_fixture + -> { IO::Buffer.map(@file, 4, "4096") }.should.raise(TypeError, /no implicit conversion/) + -> { IO::Buffer.map(@file, 4, nil) }.should.raise(TypeError, /no implicit conversion/) + 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(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(fixture(__dir__, "read_text.txt"), "rb") + @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(IO::Buffer::AccessError, "Buffer is not writable!") + end + end + + 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".b + end + + it "allows mapping read-only files and modifying the buffer" do + @file = File.open(fixture(__dir__, "read_text.txt"), "rb") + @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".b + 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 diff --git a/spec/ruby/core/io/buffer/mapped_spec.rb b/spec/ruby/core/io/buffer/mapped_spec.rb new file mode 100644 index 0000000000..0decb97704 --- /dev/null +++ b/spec/ruby/core/io/buffer/mapped_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#mapped?" do + after :each do + @buffer&.free + @buffer = nil + end + + it "is true for a buffer with mapped memory" do + @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED) + @buffer.mapped?.should == true + end + + it "is false for a buffer with non-mapped memory" do + @buffer = IO::Buffer.for("string") + @buffer.mapped?.should == false + end + + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.mapped?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/not_spec.rb b/spec/ruby/core/io/buffer/not_spec.rb new file mode 100644 index 0000000000..4737a30bde --- /dev/null +++ b/spec/ruby/core/io/buffer/not_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../../spec_helper' + +describe :io_buffer_not, shared: true do + it "inverts every bit of the buffer" do + IO::Buffer.for(+"12345") do |buffer| + result = buffer.send(@method) + result.get_string.should == "\xCE\xCD\xCC\xCB\xCA".b + result.free + end + end +end + +describe "IO::Buffer#~" do + it_behaves_like :io_buffer_not, :~ + + it "creates a new internal buffer of the same size" do + IO::Buffer.for(+"12345") do |buffer| + result = ~buffer + result.should_not.equal? buffer + result.should.internal? + result.size.should == buffer.size + result.free + end + end +end + +describe "IO::Buffer#not!" do + it_behaves_like :io_buffer_not, :not! + + it "modifies the buffer in place" do + IO::Buffer.for(+"12345") do |buffer| + result = buffer.not! + result.should.equal? buffer + result.should.external? + end + end +end diff --git a/spec/ruby/core/io/buffer/null_spec.rb b/spec/ruby/core/io/buffer/null_spec.rb new file mode 100644 index 0000000000..380a98bde1 --- /dev/null +++ b/spec/ruby/core/io/buffer/null_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../../spec_helper' +require_relative 'shared/null_and_empty' + +describe "IO::Buffer#null?" do + after :each do + @buffer&.free + @buffer = nil + end + + it_behaves_like :io_buffer_null_and_empty, :null? + + it "is false for a 0-length String-backed buffer created with .for" do + @buffer = IO::Buffer.for("") + @buffer.null?.should == false + end + + it "is false for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.null?.should == false + end + end + + it "is false for a 0-length slice of a buffer with size > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(3, 0).null?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/or_spec.rb b/spec/ruby/core/io/buffer/or_spec.rb new file mode 100644 index 0000000000..3e627c216c --- /dev/null +++ b/spec/ruby/core/io/buffer/or_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../../spec_helper' + +describe :io_buffer_or, shared: true do + it "applies the argument buffer as an OR bit mask across the whole buffer" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer.send(@method, mask) + result.get_string.should == "\xF9\xBF\xFB\xBF\xFD".b + result.free + end + end + end + + it "ignores extra parts of mask if it is longer than source buffer" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F\x00\x00\x00\xFF\xFF") do |mask| + result = buffer.send(@method, mask) + result.get_string.should == "\xF9\xBF345".b + result.free + end + end + end + + it "raises TypeError if mask is not an IO::Buffer" do + IO::Buffer.for(+"12345") do |buffer| + -> { buffer.send(@method, "\xF8\x8F") }.should.raise(TypeError, "wrong argument type String (expected IO::Buffer)") + -> { buffer.send(@method, 0xF8) }.should.raise(TypeError, "wrong argument type Integer (expected IO::Buffer)") + -> { buffer.send(@method, nil) }.should.raise(TypeError, "wrong argument type nil (expected IO::Buffer)") + end + end +end + +describe "IO::Buffer#|" do + it_behaves_like :io_buffer_or, :| + + it "creates a new internal buffer of the same size" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer | mask + result.should_not.equal? buffer + result.should.internal? + result.size.should == buffer.size + result.free + buffer.get_string.should == "12345".b + end + end + end +end + +describe "IO::Buffer#or!" do + it_behaves_like :io_buffer_or, :or! + + it "modifies the buffer in place" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer.or!(mask) + result.should.equal? buffer + result.should.external? + end + end + end +end diff --git a/spec/ruby/core/io/buffer/private_spec.rb b/spec/ruby/core/io/buffer/private_spec.rb new file mode 100644 index 0000000000..6e6afee34c --- /dev/null +++ b/spec/ruby/core/io/buffer/private_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#private?" do + after :each do + @buffer&.free + @buffer = nil + 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 == true + end + + it "is false for a buffer created without PRIVATE flag" do + @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL) + @buffer.private?.should == false + end + + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.private?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/readonly_spec.rb b/spec/ruby/core/io/buffer/readonly_spec.rb new file mode 100644 index 0000000000..4eefc9f29f --- /dev/null +++ b/spec/ruby/core/io/buffer/readonly_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#readonly?" do + after :each do + @buffer&.free + @buffer = nil + 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 == true + end + + it "is true for a buffer that is non-writable" do + @buffer = IO::Buffer.for("string") + @buffer.readonly?.should == true + end + + it "is false for a modifiable buffer" do + @buffer = IO::Buffer.new(12) + @buffer.readonly?.should == false + end + + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.readonly?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb new file mode 100644 index 0000000000..6e684475f3 --- /dev/null +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -0,0 +1,151 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#resize" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "resizes internal buffer, preserving type" do + @buffer = IO::Buffer.new(4) + @buffer.resize(IO::Buffer::PAGE_SIZE) + @buffer.size.should == IO::Buffer::PAGE_SIZE + @buffer.internal?.should == true + @buffer.mapped?.should == false + end + + platform_is :linux do + it "resizes mapped buffer, preserving type" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED) + @buffer.resize(4) + @buffer.size.should == 4 + @buffer.internal?.should == false + @buffer.mapped?.should == true + end + end + + platform_is_not :linux do + it "resizes mapped buffer, changing type to internal" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED) + @buffer.resize(4) + @buffer.size.should == 4 + @buffer.internal?.should == true + @buffer.mapped?.should == false + end + end + end + + context "with a file-backed buffer created with .map" do + it "disallows resizing shared buffer, raising IO::Buffer::AccessError" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + -> { @buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + it "resizes private buffer, discarding excess contents" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.resize(10) + @buffer.size.should == 10 + @buffer.get_string.should == "require_re" + @buffer.resize(12) + @buffer.size.should == 12 + @buffer.get_string.should == "require_re\0\0" + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "disallows resizing, raising IO::Buffer::AccessError" do + @buffer = IO::Buffer.for(+"test") + -> { @buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + context "with a block" do + it "disallows resizing, raising IO::Buffer::AccessError" do + IO::Buffer.for(+'test') do |buffer| + -> { buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + end + + context "with a String-backed buffer created with .string" do + it "disallows resizing, raising IO::Buffer::AccessError" do + IO::Buffer.string(4) do |buffer| + -> { buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + + context "with a null buffer" do + it "allows resizing a 0-sized buffer, creating a regular buffer according to new size" do + @buffer = IO::Buffer.new(0) + @buffer.resize(IO::Buffer::PAGE_SIZE) + @buffer.size.should == IO::Buffer::PAGE_SIZE + @buffer.internal?.should == false + @buffer.mapped?.should == true + end + + it "allows resizing after a free, creating a regular buffer according to new size" do + @buffer = IO::Buffer.for("test") + @buffer.free + @buffer.resize(10) + @buffer.size.should == 10 + @buffer.internal?.should == true + @buffer.mapped?.should == false + end + end + + it "allows resizing to 0, freeing memory" do + @buffer = IO::Buffer.new(4) + @buffer.resize(0) + @buffer.null?.should == true + end + + it "can be called repeatedly" do + @buffer = IO::Buffer.new(4) + @buffer.resize(10) + @buffer.resize(27) + @buffer.resize(1) + @buffer.size.should == 1 + end + + it "always clears extra memory" do + @buffer = IO::Buffer.new(4) + @buffer.set_string("test") + # This should not cause a re-allocation, just a technical resizing, + # even with very aggressive memory allocation. + @buffer.resize(2) + @buffer.resize(4) + @buffer.get_string.should == "te\0\0" + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.resize(10) }.should.raise(IO::Buffer::LockedError, "Cannot resize locked buffer!") + end + end + + it "raises ArgumentError if size is negative" do + @buffer = IO::Buffer.new(4) + -> { @buffer.resize(-1) }.should.raise(ArgumentError, "Size can't be negative!") + end + + it "raises TypeError if size is not an Integer" do + @buffer = IO::Buffer.new(4) + -> { @buffer.resize(nil) }.should.raise(TypeError, "not an Integer") + -> { @buffer.resize(10.0) }.should.raise(TypeError, "not an Integer") + end + + context "with a slice of a buffer" do + # Current behavior of slice resizing seems unintended (it's undocumented, too). + # It either creates a completely new buffer, or breaks the slice on size 0. + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/io/buffer/shared/null_and_empty.rb b/spec/ruby/core/io/buffer/shared/null_and_empty.rb new file mode 100644 index 0000000000..f8abc5f0fc --- /dev/null +++ b/spec/ruby/core/io/buffer/shared/null_and_empty.rb @@ -0,0 +1,57 @@ +describe :io_buffer_null_and_empty, shared: true do + it "is false for a buffer with size > 0" do + @buffer = IO::Buffer.new(1) + @buffer.send(@method).should == false + end + + it "is false for a slice with length > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(1, 2).send(@method).should == false + end + + it "is false for a file-mapped buffer" do + File.open(__FILE__, "rb") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.send(@method).should == false + end + end + + it "is false for a non-empty String-backed buffer created with .for" do + @buffer = IO::Buffer.for("test") + @buffer.send(@method).should == false + end + + it "is false for a non-empty String-backed buffer created with .string" do + IO::Buffer.string(4) do |buffer| + buffer.send(@method).should == false + end + end + + it "is true for a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.send(@method).should == true + end + + it "is true for a slice of a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.slice(0, 0).send(@method).should == true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(1) + @buffer.free + @buffer.send(@method).should == true + end + + it "is true for a buffer resized to 0" do + @buffer = IO::Buffer.new(1) + @buffer.resize(0) + @buffer.send(@method).should == true + end + + it "is true for a buffer whose memory was transferred" do + buffer = IO::Buffer.new(1) + @buffer = buffer.transfer + buffer.send(@method).should == true + end +end diff --git a/spec/ruby/core/io/buffer/shared_spec.rb b/spec/ruby/core/io/buffer/shared_spec.rb new file mode 100644 index 0000000000..daaa66c5ad --- /dev/null +++ b/spec/ruby/core/io/buffer/shared_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#shared?" do + after :each do + @buffer&.free + @buffer = nil + 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 == true + end + + it "is true for a non-private buffer created with .map" do + path = tmp("read_text.txt") + File.copy_stream(fixture(__dir__, "read_text.txt"), path) + file = File.open(path, "r+") + @buffer = IO::Buffer.map(file) + @buffer.shared?.should == true + ensure + @buffer.free + file.close + File.unlink(path) + end + + it "is false for an unshared buffer" do + @buffer = IO::Buffer.new(12) + @buffer.shared?.should == false + end + + it "is false for a null buffer" do + @buffer = IO::Buffer.new(0) + @buffer.shared?.should == false + end +end diff --git a/spec/ruby/core/io/buffer/string_spec.rb b/spec/ruby/core/io/buffer/string_spec.rb new file mode 100644 index 0000000000..4c73ba5e1e --- /dev/null +++ b/spec/ruby/core/io/buffer/string_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../../spec_helper' + +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(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(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(LocalJumpError, "no block given") + end +end diff --git a/spec/ruby/core/io/buffer/transfer_spec.rb b/spec/ruby/core/io/buffer/transfer_spec.rb new file mode 100644 index 0000000000..02e029016a --- /dev/null +++ b/spec/ruby/core/io/buffer/transfer_spec.rb @@ -0,0 +1,116 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#transfer" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "transfers internal memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.new(4) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should == true + end + + it "transfers mapped memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should == true + end + end + + context "with a file-backed buffer created with .map" do + it "transfers mapped memory to a new buffer, nullifying the original" do + File.open(__FILE__, "r") do |file| + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should == true + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "transfers memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.for("test") + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should == true + end + end + + context "with a block" do + it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do + IO::Buffer.for(+"test") do |buffer| + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should == true + end + @buffer.null?.should == false + end + end + end + + context "with a String-backed buffer created with .string" do + it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do + IO::Buffer.string(4) do |buffer| + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should == true + end + @buffer.null?.should == false + end + end + + it "allows multiple transfers" do + buffer_1 = IO::Buffer.new(4) + buffer_2 = buffer_1.transfer + @buffer = buffer_2.transfer + buffer_1.null?.should == true + buffer_2.null?.should == true + @buffer.null?.should == false + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.transfer }.should.raise(IO::Buffer::LockedError, "Cannot transfer ownership of locked buffer!") + end + end + + context "with a slice of a buffer" do + it "transfers source to a new slice, not touching the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.set_string("test") + + new_slice = slice.transfer + slice.null?.should == true + new_slice.null?.should == false + @buffer.null?.should == false + + new_slice.set_string("ea") + @buffer.get_string.should == "east" + end + + it "nullifies buffer, invalidating the slice" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + @buffer = buffer.transfer + + slice.null?.should == false + slice.valid?.should == false + -> { slice.get_string }.should.raise(IO::Buffer::InvalidatedError, "Buffer has been invalidated!") + end + end +end diff --git a/spec/ruby/core/io/buffer/valid_spec.rb b/spec/ruby/core/io/buffer/valid_spec.rb new file mode 100644 index 0000000000..0a40172869 --- /dev/null +++ b/spec/ruby/core/io/buffer/valid_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#valid?" do + after :each do + @buffer&.free + @buffer = nil + end + + # Non-slices are always valid + context "with a non-slice buffer" do + it "is true for a regular buffer" do + @buffer = IO::Buffer.new(4) + @buffer.valid?.should == true + end + + it "is true for a 0-size buffer" do + @buffer = IO::Buffer.new(0) + @buffer.valid?.should == true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(4) + @buffer.free + @buffer.valid?.should == true + end + + it "is true for a freed file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.valid?.should == true + @buffer.free + @buffer.valid?.should == true + end + end + + it "is true for a freed string-backed buffer" do + @buffer = IO::Buffer.for("hello") + @buffer.valid?.should == true + @buffer.free + @buffer.valid?.should == true + end + end + + # "A buffer becomes invalid if it is a slice of another buffer (or string) + # which has been freed or re-allocated at a different address." + context "with a slice" do + it "is true for a slice of a live buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + slice.valid?.should == true + end + + context "when buffer is resized" do + it "is false when slice becomes outside the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(2, 2) + @buffer.resize(3) + slice.valid?.should == false + end + + platform_is_not :linux do + # This test does not cause a copy-resize on Linux. + # `#resize` MAY cause the buffer to move, but there is no guarantee. + it "is false when buffer is copied on resize" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + slice = @buffer.slice(0, 2) + @buffer.resize(8) + slice.valid?.should == false + end + end + end + + it "is false for a slice of a transferred buffer" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + @buffer = buffer.transfer + slice.valid?.should == false + end + + it "is false for a slice of a freed buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.free + slice.valid?.should == false + end + + it "is false for a slice of a freed file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + slice = @buffer.slice(0, 2) + slice.valid?.should == true + @buffer.free + slice.valid?.should == false + end + end + + it "is true for a slice of a freed string-backed buffer while string is alive" do + @buffer = IO::Buffer.for("alive") + slice = @buffer.slice(0, 2) + slice.valid?.should == true + @buffer.free + slice.valid?.should == true + end + + # There probably should be a test with a garbage-collected string, + # but it's not clear how to force that. + + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/io/buffer/xor_spec.rb b/spec/ruby/core/io/buffer/xor_spec.rb new file mode 100644 index 0000000000..9287611f7f --- /dev/null +++ b/spec/ruby/core/io/buffer/xor_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../../spec_helper' + +describe :io_buffer_xor, shared: true do + it "applies the argument buffer as an XOR bit mask across the whole buffer" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer.send(@method, mask) + result.get_string.should == "\xC9\xBD\xCB\xBB\xCD".b + result.free + end + end + end + + it "ignores extra parts of mask if it is longer than source buffer" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F\x00\x00\x00\xFF\xFF") do |mask| + result = buffer.send(@method, mask) + result.get_string.should == "\xC9\xBD345".b + result.free + end + end + end + + it "raises TypeError if mask is not an IO::Buffer" do + IO::Buffer.for(+"12345") do |buffer| + -> { buffer.send(@method, "\xF8\x8F") }.should.raise(TypeError, "wrong argument type String (expected IO::Buffer)") + -> { buffer.send(@method, 0xF8) }.should.raise(TypeError, "wrong argument type Integer (expected IO::Buffer)") + -> { buffer.send(@method, nil) }.should.raise(TypeError, "wrong argument type nil (expected IO::Buffer)") + end + end +end + +describe "IO::Buffer#^" do + it_behaves_like :io_buffer_xor, :^ + + it "creates a new internal buffer of the same size" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer ^ mask + result.should_not.equal? buffer + result.should.internal? + result.size.should == buffer.size + result.free + buffer.get_string.should == "12345".b + end + end + end +end + +describe "IO::Buffer#xor!" do + it_behaves_like :io_buffer_xor, :xor! + + it "modifies the buffer in place" do + IO::Buffer.for(+"12345") do |buffer| + IO::Buffer.for(+"\xF8\x8F") do |mask| + result = buffer.xor!(mask) + result.should.equal? buffer + result.should.external? + end + end + end +end |
