Skip to content

Conversation

@trinistr
Copy link
Contributor

@trinistr trinistr commented Nov 7, 2025

This class currently has no specs at all. This PR adds specs for the following methods:

  • .new,
  • #free, #resize, #transfer,
  • all predicate methods,
  • incidental tests for many other methods, including .map, .for, .strings and some instance methods.

Special remarks:

  • spec for #valid? is somewhat incomplete, as garbage-collecting a string is non-trivial (I don't know how to);
  • spec for #resize does not have tests for slices as currently it seems to be an undefined behavior, and it's not clear if it should be reproduced (yes, ruby/spec is all about reproducing even weird behavior, but bugs are a gray area still).

Weird things discovered so far so I don't forget:

  • .new allows any and all flags (https://bugs.ruby-lang.org/issues/21672).
  • Documentation is wrong (.map, for example, has very early text).
  • INTERNAL and EXTERNAL are unclear, not being opposites (both can be false at the same time) and not even forming an exclusive tri-state with MAPPED.
  • Slices feel undercooked:
    • No flags are copied from the source, except READONLY.
    • No way to detect a slice except to read #to_s (or call all predicate methods to check for all-false).
    • Is calling #free even safe on a slice? (io_buffer_free seems to work fine only because no flags are copied, not actually checking "is this a slice?").
    • Calling #free on the source buffer is not safe (https://bugs.ruby-lang.org/issues/21212).
    • And #lockeding does not propagate in either direction.
    • #freeing a String-backed buffer does not invalidate its slice (shouldn't slices always become invalid without a source buffer?).
    • Slices can be resized at any point?! (Breaks the slice if size is 0, otherwise creates a new internal buffer. (Again, this is a case of "not checking for slice, and no flags are set".))
  • Is SHARED intended to be used manually or not?
  • Why is PRIVATE mapping neither INTERNAL or EXTERNAL?
  • .for and .string with a block are supposed to nullify buffer at the end, but buffer can be #transferred outside, is this intentional? (this creates a run-away buffer, able to change the string from wherever (but not a frozen string, at least)).
  • #resize on a MAPPED buffer changes it to INTERNAL on macOS and Windows.
@trinistr trinistr force-pushed the add-io-buffer-spec branch 3 times, most recently from e0188af to 4ce76f1 Compare November 7, 2025 21:14
Comment on lines 75 to 82
# This looks like a bug to me, these should be different memory models?
# Definitely looks like a bug, created issue: https://bugs.ruby-lang.org/issues/21672
it "creates internal-mapped buffer if INTERNAL and MAPPED are both specified" do
@buffer = IO::Buffer.new(10, IO::Buffer::INTERNAL | IO::Buffer::MAPPED)
@buffer.should.internal?
@buffer.should.mapped?
@buffer.should_not.empty?
end
Copy link
Contributor Author

@trinistr trinistr Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very clearly a bug, so I filed it as such. Not sure if the test needs to be here. Probably not? This does not seem like behavior that should be replicated. I will delete it for now.

@trinistr trinistr force-pushed the add-io-buffer-spec branch 3 times, most recently from c307df9 to 840258b Compare November 9, 2025 11:43
@trinistr trinistr changed the title Add specs for IO::Buffer Add initial specs for IO::Buffer Nov 9, 2025
@trinistr trinistr marked this pull request as ready for review November 9, 2025 12:34
@buffer.size.should == size
@buffer.should.mapped?
@buffer.should_not.empty?
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: if given size affects both "internal" and "mapped" state - it makes sense to check both of them in each of the two tests above.

it "raises IO::Buffer::AllocationError with other values for flags" 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!")
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: This test case doesn't look accurate. It seems the exception is raised only if neither IO::Buffer::MAPPED nor IO::Buffer::MAPPED flag is given.

https://github.com/ruby/ruby/blob/7a5688e2a27668e48f8d6ff4af5b2208b98a2f5e/io_buffer.c#L202-L213

All the other known flags are applied

https://github.com/ruby/ruby/blob/7a5688e2a27668e48f8d6ff4af5b2208b98a2f5e/io_buffer.c#L47.

And it seems unknown flags are just ignored.

@buffer = IO::Buffer.new(4, IO::Buffer::MAPPED)
@buffer.external?.should be_false
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: It seems to me that this logic is closer to the constructor methods specs (new/map/for) than to the predicates. I would keep here just a couple of examples when a predicate returns either true or false.

end
buffer.free
buffer.null?.should be_true
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: seems it's duplicated in specs for #locked?.

slice.null?.should be_false
slice.valid?.should be_false
-> { slice.get_string }.should raise_error(IO::Buffer::InvalidatedError, "Buffer has been invalidated!")
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: seems is duplicated in specs for #valid?

# See specs of individual methods for error messages.
-> { @buffer.free }.should raise_error(IO::Buffer::LockedError)
-> { @buffer.resize(8) }.should raise_error(IO::Buffer::LockedError)
-> { @buffer.transfer }.should raise_error(IO::Buffer::LockedError)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: it makes sense to specify exception messages everywhere

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: I suppose other operations changing a buffer should be mentioned here as well (e.g. set_string or write).

@buffer.locked do
-> { @buffer.resize(10) }.should raise_error(IO::Buffer::LockedError, "Cannot resize locked buffer!")
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: seems a duplicate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants