diff options
| author | Charles Oliver Nutter <headius@headius.com> | 2025-11-18 19:38:20 -0600 |
|---|---|---|
| committer | Benoit Daloze <eregontp@gmail.com> | 2025-11-19 12:36:40 +0100 |
| commit | 85cd08e4f9f422357207ace0a53b9ec8020df976 (patch) | |
| tree | 71a1ef998b0c953bfa8fc9f9509b6be5127a4533 /spec/ruby | |
| parent | bbb4c7b88bf4fca08487d6b115b1031f56ac9cf6 (diff) | |
Update to ruby/spec@2e11d2a
Diffstat (limited to 'spec/ruby')
49 files changed, 2410 insertions, 90 deletions
diff --git a/spec/ruby/core/comparable/clamp_spec.rb b/spec/ruby/core/comparable/clamp_spec.rb index 796d4a18c1..cc1df977e2 100644 --- a/spec/ruby/core/comparable/clamp_spec.rb +++ b/spec/ruby/core/comparable/clamp_spec.rb @@ -24,7 +24,7 @@ describe 'Comparable#clamp' do c.clamp(two, three).should equal(c) end - it 'returns the min parameter if smaller than it' do + it 'returns the min parameter if less than it' do one = ComparableSpecs::WithOnlyCompareDefined.new(1) two = ComparableSpecs::WithOnlyCompareDefined.new(2) c = ComparableSpecs::Weird.new(0) @@ -52,7 +52,7 @@ describe 'Comparable#clamp' do c.clamp(two..three).should equal(c) end - it 'returns the minimum value of the range parameters if smaller than it' do + it 'returns the minimum value of the range parameters if less than it' do one = ComparableSpecs::WithOnlyCompareDefined.new(1) two = ComparableSpecs::WithOnlyCompareDefined.new(2) c = ComparableSpecs::Weird.new(0) @@ -75,4 +75,70 @@ describe 'Comparable#clamp' do -> { c.clamp(one...two) }.should raise_error(ArgumentError) end + + context 'with endless range' do + it 'returns minimum value of the range parameters if less than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + zero = ComparableSpecs::WithOnlyCompareDefined.new(0) + c = ComparableSpecs::Weird.new(0) + + c.clamp(one..).should equal(one) + c.clamp(zero..).should equal(c) + end + + it 'always returns self if greater than minimum value of the range parameters' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one..).should equal(c) + c.clamp(two..).should equal(c) + end + + it 'works with exclusive range' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one...).should equal(c) + end + end + + context 'with beginless range' do + it 'returns maximum value of the range parameters if greater than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + + c.clamp(..one).should equal(one) + end + + it 'always returns self if less than maximum value of the range parameters' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + zero = ComparableSpecs::WithOnlyCompareDefined.new(0) + c = ComparableSpecs::Weird.new(0) + + c.clamp(..one).should equal(c) + c.clamp(..zero).should equal(c) + end + + it 'raises an Argument error if the range parameter is exclusive' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(0) + + -> { c.clamp(...one) }.should raise_error(ArgumentError) + end + end + + context 'with beginless-and-endless range' do + it 'always returns self' do + c = ComparableSpecs::Weird.new(1) + + c.clamp(nil..nil).should equal(c) + end + + it 'works with exclusive range' do + c = ComparableSpecs::Weird.new(2) + + c.clamp(nil...nil).should equal(c) + end + end end diff --git a/spec/ruby/core/enumerable/shared/inject.rb b/spec/ruby/core/enumerable/shared/inject.rb index aae9e06c97..8fb7e98c2b 100644 --- a/spec/ruby/core/enumerable/shared/inject.rb +++ b/spec/ruby/core/enumerable/shared/inject.rb @@ -103,7 +103,7 @@ describe :enumerable_inject, shared: true do it "without inject arguments(legacy rubycon)" do # no inject argument - EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| 999 } .should == 2 + EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| 999 }.should == 2 EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| acc }.should == 2 EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| x }.should == 2 diff --git a/spec/ruby/core/hash/compact_spec.rb b/spec/ruby/core/hash/compact_spec.rb index 76aa43949d..13371bce43 100644 --- a/spec/ruby/core/hash/compact_spec.rb +++ b/spec/ruby/core/hash/compact_spec.rb @@ -35,7 +35,7 @@ describe "Hash#compact" do hash.compact.default_proc.should == pr end - it "retains compare_by_identity_flag" do + it "retains compare_by_identity flag" do hash = {}.compare_by_identity hash.compact.compare_by_identity?.should == true hash[:a] = 1 diff --git a/spec/ruby/core/hash/constructor_spec.rb b/spec/ruby/core/hash/constructor_spec.rb index 8d29773909..0f97f7b40e 100644 --- a/spec/ruby/core/hash/constructor_spec.rb +++ b/spec/ruby/core/hash/constructor_spec.rb @@ -103,14 +103,14 @@ describe "Hash.[]" do HashSpecs::MyInitializerHash[Hash[1, 2]].should be_an_instance_of(HashSpecs::MyInitializerHash) end - it "removes the default value" do + it "does not retain the default value" do hash = Hash.new(1) Hash[hash].default.should be_nil hash[:a] = 1 Hash[hash].default.should be_nil end - it "removes the default_proc" do + it "does not retain the default_proc" do hash = Hash.new { |h, k| h[k] = [] } Hash[hash].default_proc.should be_nil hash[:a] = 1 @@ -118,10 +118,11 @@ describe "Hash.[]" do end ruby_version_is '3.3' do - it "does not retain compare_by_identity_flag" do - hash = {}.compare_by_identity + it "does not retain compare_by_identity flag" do + hash = { a: 1 }.compare_by_identity Hash[hash].compare_by_identity?.should == false - hash[:a] = 1 + + hash = {}.compare_by_identity Hash[hash].compare_by_identity?.should == false end end diff --git a/spec/ruby/core/hash/except_spec.rb b/spec/ruby/core/hash/except_spec.rb index ac84f9975c..026e454b13 100644 --- a/spec/ruby/core/hash/except_spec.rb +++ b/spec/ruby/core/hash/except_spec.rb @@ -19,14 +19,24 @@ describe "Hash#except" do @hash.except(:a, :chunky_bacon).should == { b: 2, c: 3 } end - it "always returns a Hash without a default" do - klass = Class.new(Hash) - h = klass.new(:default) - h[:bar] = 12 - h[:foo] = 42 - r = h.except(:foo) - r.should == {bar: 12} - r.class.should == Hash - r.default.should == nil + it "does not retain the default value" do + h = Hash.new(1) + h.except(:a).default.should be_nil + h[:a] = 1 + h.except(:a).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.except(:a).default_proc.should be_nil + h[:a] = 1 + h.except(:a).default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.except(:a) + h2.compare_by_identity?.should == true end end diff --git a/spec/ruby/core/hash/invert_spec.rb b/spec/ruby/core/hash/invert_spec.rb index 73377a9e97..c06e15ff7c 100644 --- a/spec/ruby/core/hash/invert_spec.rb +++ b/spec/ruby/core/hash/invert_spec.rb @@ -24,4 +24,25 @@ describe "Hash#invert" do HashSpecs::MyHash[1 => 2, 3 => 4].invert.class.should == Hash HashSpecs::MyHash[].invert.class.should == Hash end + + it "does not retain the default value" do + h = Hash.new(1) + h.invert.default.should be_nil + h[:a] = 1 + h.invert.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.invert.default_proc.should be_nil + h[:a] = 1 + h.invert.default_proc.should be_nil + end + + it "does not retain compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.invert + h2.compare_by_identity?.should == false + end end diff --git a/spec/ruby/core/hash/merge_spec.rb b/spec/ruby/core/hash/merge_spec.rb index 5521864297..6710d121ef 100644 --- a/spec/ruby/core/hash/merge_spec.rb +++ b/spec/ruby/core/hash/merge_spec.rb @@ -93,6 +93,29 @@ describe "Hash#merge" do merged.should eql(hash) merged.should_not equal(hash) end + + it "retains the default value" do + h = Hash.new(1) + h.merge(b: 1, d: 2).default.should == 1 + end + + it "retains the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.merge(b: 1, d: 2).default_proc.should == pr + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.merge(b: 1, d: 2) + h2.compare_by_identity?.should == true + end + + it "ignores compare_by_identity flag of an argument" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = { b: 1, d: 2 }.merge(h) + h2.compare_by_identity?.should == false + end end describe "Hash#merge!" do diff --git a/spec/ruby/core/hash/reject_spec.rb b/spec/ruby/core/hash/reject_spec.rb index dd8e817237..8381fc7fc1 100644 --- a/spec/ruby/core/hash/reject_spec.rb +++ b/spec/ruby/core/hash/reject_spec.rb @@ -44,6 +44,27 @@ describe "Hash#reject" do reject_pairs.should == reject_bang_pairs end + it "does not retain the default value" do + h = Hash.new(1) + h.reject { false }.default.should be_nil + h[:a] = 1 + h.reject { false }.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.reject { false }.default_proc.should be_nil + h[:a] = 1 + h.reject { false }.default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.reject { |k, _| k == :a } + h2.compare_by_identity?.should == true + end + it_behaves_like :hash_iteration_no_block, :reject it_behaves_like :enumeratorized_with_origin_size, :reject, { 1 => 2, 3 => 4, 5 => 6 } end diff --git a/spec/ruby/core/hash/replace_spec.rb b/spec/ruby/core/hash/replace_spec.rb index a26a31f5f9..db30145e1a 100644 --- a/spec/ruby/core/hash/replace_spec.rb +++ b/spec/ruby/core/hash/replace_spec.rb @@ -23,39 +23,48 @@ describe "Hash#replace" do h.should == { 1 => 2 } end - it "transfers the compare_by_identity flag" do - hash_a = { a: 1 } - hash_b = { b: 2 } - hash_b.compare_by_identity - hash_a.should_not.compare_by_identity? - hash_a.replace(hash_b) - hash_a.should.compare_by_identity? + it "does not retain the default value" do + hash = Hash.new(1) + hash.replace(b: 2).default.should be_nil + end - hash_a = { a: 1 } - hash_b = { b: 2 } - hash_a.compare_by_identity - hash_a.should.compare_by_identity? - hash_a.replace(hash_b) - hash_a.should_not.compare_by_identity? + it "transfers the default value of an argument" do + hash = Hash.new(1) + { a: 1 }.replace(hash).default.should == 1 end - it "does not transfer default values" do - hash_a = {} - hash_b = Hash.new(5) - hash_a.replace(hash_b) - hash_a.default.should == 5 + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + hash.replace(b: 2).default_proc.should be_nil + end - hash_a = {} - hash_b = Hash.new { |h, k| k * 2 } - hash_a.replace(hash_b) - hash_a.default(5).should == 10 + it "transfers the default_proc of an argument" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + { a: 1 }.replace(hash).default_proc.should == pr + end + it "does not call the default_proc of an argument" do hash_a = Hash.new { |h, k| k * 5 } hash_b = Hash.new(-> { raise "Should not invoke lambda" }) hash_a.replace(hash_b) hash_a.default.should == hash_b.default end + it "transfers compare_by_identity flag of an argument" do + h = { a: 1, c: 3 } + h2 = { b: 2, d: 4 }.compare_by_identity + h.replace(h2) + h.compare_by_identity?.should == true + end + + it "does not retain compare_by_identity flag" do + h = { a: 1, c: 3 }.compare_by_identity + h.replace(b: 2, d: 4) + h.compare_by_identity?.should == false + end + it "raises a FrozenError if called on a frozen instance that would not be modified" do -> do HashSpecs.frozen_hash.replace(HashSpecs.frozen_hash) diff --git a/spec/ruby/core/hash/shared/select.rb b/spec/ruby/core/hash/shared/select.rb index 5170af50d6..fbeff07330 100644 --- a/spec/ruby/core/hash/shared/select.rb +++ b/spec/ruby/core/hash/shared/select.rb @@ -40,6 +40,27 @@ describe :hash_select, shared: true do @empty.send(@method).should be_an_instance_of(Enumerator) end + it "does not retain the default value" do + h = Hash.new(1) + h.send(@method) { true }.default.should be_nil + h[:a] = 1 + h.send(@method) { true }.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.send(@method) { true }.default_proc.should be_nil + h[:a] = 1 + h.send(@method) { true }.default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.send(@method) { |k, _| k == :a } + h2.compare_by_identity?.should == true + end + it_should_behave_like :hash_iteration_no_block before :each do diff --git a/spec/ruby/core/hash/slice_spec.rb b/spec/ruby/core/hash/slice_spec.rb index e3046d83d7..4fcc01f9a6 100644 --- a/spec/ruby/core/hash/slice_spec.rb +++ b/spec/ruby/core/hash/slice_spec.rb @@ -50,4 +50,25 @@ describe "Hash#slice" do ScratchPad.recorded.should == [] end + + it "does not retain the default value" do + h = Hash.new(1) + h.slice(:a).default.should be_nil + h[:a] = 1 + h.slice(:a).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.slice(:a).default_proc.should be_nil + h[:a] = 1 + h.slice(:a).default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.slice(:a) + h2.compare_by_identity?.should == true + end end diff --git a/spec/ruby/core/hash/to_h_spec.rb b/spec/ruby/core/hash/to_h_spec.rb index e17ca7e671..f84fd7b503 100644 --- a/spec/ruby/core/hash/to_h_spec.rb +++ b/spec/ruby/core/hash/to_h_spec.rb @@ -19,17 +19,22 @@ describe "Hash#to_h" do @h[:foo].should == :bar end - it "copies the default" do + it "retains the default" do @h.default = 42 @h.to_h.default.should == 42 @h[:hello].should == 42 end - it "copies the default_proc" do + it "retains the default_proc" do @h.default_proc = prc = Proc.new{ |h, k| h[k] = 2 * k } @h.to_h.default_proc.should == prc @h[42].should == 84 end + + it "retains compare_by_identity flag" do + @h.compare_by_identity + @h.to_h.compare_by_identity?.should == true + end end context "with block" do @@ -78,5 +83,24 @@ describe "Hash#to_h" do { a: 1 }.to_h { |k| x } end.should raise_error(TypeError, /wrong element type MockObject/) end + + it "does not retain the default value" do + h = Hash.new(1) + h2 = h.to_h { |k, v| [k.to_s, v*v]} + h2.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h2 = h.to_h { |k, v| [k.to_s, v*v]} + h2.default_proc.should be_nil + end + + it "does not retain compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.to_h { |k, v| [k.to_s, v*v]} + h2.compare_by_identity?.should == false + end end end diff --git a/spec/ruby/core/hash/transform_keys_spec.rb b/spec/ruby/core/hash/transform_keys_spec.rb index f63d39ecc8..e2eeab1813 100644 --- a/spec/ruby/core/hash/transform_keys_spec.rb +++ b/spec/ruby/core/hash/transform_keys_spec.rb @@ -54,6 +54,27 @@ describe "Hash#transform_keys" do it "allows a combination of hash and block argument" do @hash.transform_keys({ a: :A }, &:to_s).should == { A: 1, 'b' => 2, 'c' => 3 } end + + it "does not retain the default value" do + h = Hash.new(1) + h.transform_keys(&:succ).default.should be_nil + h[:a] = 1 + h.transform_keys(&:succ).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.transform_values(&:succ).default_proc.should be_nil + h[:a] = 1 + h.transform_values(&:succ).default_proc.should be_nil + end + + it "does not retain compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.transform_keys(&:succ) + h2.compare_by_identity?.should == false + end end describe "Hash#transform_keys!" do diff --git a/spec/ruby/core/hash/transform_values_spec.rb b/spec/ruby/core/hash/transform_values_spec.rb index acb469416a..4a0ae8a5a5 100644 --- a/spec/ruby/core/hash/transform_values_spec.rb +++ b/spec/ruby/core/hash/transform_values_spec.rb @@ -39,6 +39,27 @@ describe "Hash#transform_values" do r[:foo].should == 84 r.class.should == Hash end + + it "does not retain the default value" do + h = Hash.new(1) + h.transform_values(&:succ).default.should be_nil + h[:a] = 1 + h.transform_values(&:succ).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.transform_values(&:succ).default_proc.should be_nil + h[:a] = 1 + h.transform_values(&:succ).default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.transform_values(&:succ) + h2.compare_by_identity?.should == true + end end describe "Hash#transform_values!" do 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..e1fd4ab6a2 --- /dev/null +++ b/spec/ruby/core/io/buffer/empty_spec.rb @@ -0,0 +1,29 @@ +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 be_true + end + + ruby_version_is "3.3" do + it "is true for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.empty?.should be_true + end + 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 be_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..4377a38357 --- /dev/null +++ b/spec/ruby/core/io/buffer/external_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#external?" do + after :each do + @buffer&.free + @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 + 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 + 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 + 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..f3a4918978 --- /dev/null +++ b/spec/ruby/core/io/buffer/free_spec.rb @@ -0,0 +1,104 @@ +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 be_true + end + + it "frees mapped memory and nullifies the buffer" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + buffer.free + buffer.null?.should be_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 be_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 be_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 be_true + end + string.should == "meat" + end + end + end + + ruby_version_is "3.3" do + 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 be_true + end + string.should == "meat" + end + end + end + + it "can be called repeatedly without an error" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should be_true + buffer.free + buffer.null?.should be_true + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + buffer = IO::Buffer.new(4) + buffer.locked do + -> { buffer.free }.should raise_error(IO::Buffer::LockedError, "Buffer is locked!") + end + buffer.free + buffer.null?.should be_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 be_true + buffer.null?.should be_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 be_false + slice.valid?.should be_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..c86d1e7f1d --- /dev/null +++ b/spec/ruby/core/io/buffer/initialize_spec.rb @@ -0,0 +1,103 @@ +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.shared? + @buffer.should_not.readonly? + + @buffer.should_not.empty? + @buffer.should_not.null? + + # This is run-time state, set by #locked. + @buffer.should_not.locked? + 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.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.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 + + it "raises TypeError if size is not an Integer" do + -> { IO::Buffer.new(nil) }.should raise_error(TypeError, "not an Integer") + -> { IO::Buffer.new(10.0) }.should raise_error(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + -> { IO::Buffer.new(-1) }.should raise_error(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 "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!") + end + + ruby_version_is "3.3" do + it "raises ArgumentError if flags is negative" do + -> { IO::Buffer.new(10, -1) }.should raise_error(ArgumentError, "Flags can't be negative!") + end + end + + ruby_version_is ""..."3.3" do + it "raises IO::Buffer::AllocationError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + end + + ruby_version_is "3.3" do + it "raises TypeError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should raise_error(TypeError, "not an Integer") + end + 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..409699cc3c --- /dev/null +++ b/spec/ruby/core/io/buffer/internal_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#internal?" do + after :each do + @buffer&.free + @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 + 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 + 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 + 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..4ffa569fd2 --- /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_error(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_error(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 be_true + slice.locked?.should be_false + slice.locked { slice.locked?.should be_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 be_true + @buffer.locked?.should be_false + @buffer.locked { @buffer.locked?.should be_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 be_false + end + + it "is true only inside of #locked block" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + @buffer.locked?.should be_true + end + @buffer.locked?.should be_false + 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..b3610207ff --- /dev/null +++ b/spec/ruby/core/io/buffer/mapped_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#mapped?" do + after :each do + @buffer&.free + @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 + 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 + 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 + 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..3fb1144d0e --- /dev/null +++ b/spec/ruby/core/io/buffer/null_spec.rb @@ -0,0 +1,29 @@ +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 be_false + end + + ruby_version_is "3.3" do + it "is false for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.null?.should be_false + end + 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 be_false + 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..7aa308997b --- /dev/null +++ b/spec/ruby/core/io/buffer/private_spec.rb @@ -0,0 +1,111 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "IO::Buffer#private?" do + after :each do + @buffer&.free + @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 + 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 + 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 + end + 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..0014a876ed --- /dev/null +++ b/spec/ruby/core/io/buffer/readonly_spec.rb @@ -0,0 +1,143 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#readonly?" do + after :each do + @buffer&.free + @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 + 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 + 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 + 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 + 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..0da3a23356 --- /dev/null +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -0,0 +1,155 @@ +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 be_true + @buffer.mapped?.should be_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 be_false + @buffer.mapped?.should be_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 be_true + @buffer.mapped?.should be_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_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + ruby_version_is "3.3" do + 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 + 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_error(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_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + end + + ruby_version_is "3.3" do + 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_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + 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 be_false + @buffer.mapped?.should be_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 be_true + @buffer.mapped?.should be_false + end + end + + it "allows resizing to 0, freeing memory" do + @buffer = IO::Buffer.new(4) + @buffer.resize(0) + @buffer.null?.should be_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_error(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_error(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_error(TypeError, "not an Integer") + -> { @buffer.resize(10.0) }.should raise_error(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..c8fe9e5e46 --- /dev/null +++ b/spec/ruby/core/io/buffer/shared/null_and_empty.rb @@ -0,0 +1,59 @@ +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 be_false + end + + it "is false for a slice with length > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(1, 2).send(@method).should be_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 be_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 be_false + end + + ruby_version_is "3.3" do + it "is false for a non-empty String-backed buffer created with .string" do + IO::Buffer.string(4) do |buffer| + buffer.send(@method).should be_false + end + end + end + + it "is true for a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.send(@method).should be_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 be_true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(1) + @buffer.free + @buffer.send(@method).should be_true + end + + it "is true for a buffer resized to 0" do + @buffer = IO::Buffer.new(1) + @buffer.resize(0) + @buffer.send(@method).should be_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 be_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..f2a638cf39 --- /dev/null +++ b/spec/ruby/core/io/buffer/shared_spec.rb @@ -0,0 +1,117 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#shared?" do + after :each do + @buffer&.free + @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 + 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 + 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 + 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 + 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..cb8c843ff2 --- /dev/null +++ b/spec/ruby/core/io/buffer/transfer_spec.rb @@ -0,0 +1,118 @@ +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 be_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 be_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 be_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 be_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 be_true + end + @buffer.null?.should be_false + end + end + end + + ruby_version_is "3.3" do + 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 be_true + end + @buffer.null?.should be_false + end + 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 be_true + buffer_2.null?.should be_true + @buffer.null?.should be_false + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.transfer }.should raise_error(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 be_true + new_slice.null?.should be_false + @buffer.null?.should be_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 be_false + slice.valid?.should be_false + -> { slice.get_string }.should raise_error(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..695415dff6 --- /dev/null +++ b/spec/ruby/core/io/buffer/valid_spec.rb @@ -0,0 +1,119 @@ +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 be_true + end + + it "is true for a 0-size buffer" do + @buffer = IO::Buffer.new(0) + @buffer.valid?.should be_true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(4) + @buffer.free + @buffer.valid?.should be_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 be_true + @buffer.free + @buffer.valid?.should be_true + end + end + + it "is true for a freed string-backed buffer" do + @buffer = IO::Buffer.for("hello") + @buffer.valid?.should be_true + @buffer.free + @buffer.valid?.should be_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 be_true + end + + context "when buffer is resized" do + platform_is_not :windows do + it "is true when slice is still inside the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(1, 2) + @buffer.resize(3) + slice.valid?.should be_true + end + end + + 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 be_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 be_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 be_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 be_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 be_true + @buffer.free + slice.valid?.should be_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 be_true + @buffer.free + slice.valid?.should be_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/kernel/Float_spec.rb b/spec/ruby/core/kernel/Float_spec.rb index e00fe81572..74f6f1b0bd 100644 --- a/spec/ruby/core/kernel/Float_spec.rb +++ b/spec/ruby/core/kernel/Float_spec.rb @@ -231,6 +231,10 @@ describe :kernel_float, shared: true do @object.send(:Float, "0x0f").should == 15.0 end + it "interprets negative hex value" do + @object.send(:Float, "-0x10").should == -16.0 + end + it "accepts embedded _ if the number does not contain a-f" do @object.send(:Float, "0x1_0").should == 16.0 end diff --git a/spec/ruby/core/kernel/autoload_spec.rb b/spec/ruby/core/kernel/autoload_spec.rb index 0404caec6d..5edb70541d 100644 --- a/spec/ruby/core/kernel/autoload_spec.rb +++ b/spec/ruby/core/kernel/autoload_spec.rb @@ -7,7 +7,9 @@ require_relative 'fixtures/classes' autoload :KSAutoloadA, "autoload_a.rb" autoload :KSAutoloadB, fixture(__FILE__, "autoload_b.rb") -autoload :KSAutoloadCallsRequire, "main_autoload_not_exist.rb" +define_autoload_KSAutoloadCallsRequire = -> { + autoload :KSAutoloadCallsRequire, "main_autoload_not_exist.rb" +} def check_autoload(const) autoload? const @@ -43,6 +45,7 @@ describe "Kernel#autoload" do end it "calls main.require(path) to load the file" do + define_autoload_KSAutoloadCallsRequire.call main = TOPLEVEL_BINDING.eval("self") main.should_receive(:require).with("main_autoload_not_exist.rb") # The constant won't be defined since require is mocked to do nothing diff --git a/spec/ruby/core/matchdata/bytebegin_spec.rb b/spec/ruby/core/matchdata/bytebegin_spec.rb new file mode 100644 index 0000000000..08c1fd6d1e --- /dev/null +++ b/spec/ruby/core/matchdata/bytebegin_spec.rb @@ -0,0 +1,132 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.4" do + describe "MatchData#bytebegin" do + context "when passed an integer argument" do + it "returns the byte-based offset of the start of the nth element" do + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.bytebegin(0).should == 1 + match_data.bytebegin(2).should == 2 + end + + it "returns nil when the nth match isn't found" do + match_data = /something is( not)? (right)/.match("something is right") + match_data.bytebegin(1).should be_nil + end + + it "returns the byte-based offset for multi-byte strings" do + match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") + match_data.bytebegin(0).should == 1 + match_data.bytebegin(2).should == 3 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi-byte strings with unicode regexp" do + match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") + match_data.bytebegin(0).should == 1 + match_data.bytebegin(2).should == 3 + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.bytebegin(obj).should == 2 + end + + it "raises IndexError if index is out of bounds" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.bytebegin(-1) + }.should raise_error(IndexError, "index -1 out of matches") + + -> { + match_data.bytebegin(3) + }.should raise_error(IndexError, "index 3 out of matches") + end + end + + context "when passed a String argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin("a").should == 1 + match_data.bytebegin("b").should == 3 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.bytebegin("a").should == 1 + match_data.bytebegin("b").should == 4 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.bytebegin("a").should == 1 + match_data.bytebegin("b").should == 4 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.bytebegin("a").should == 3 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin("æ").should == 1 + end + + it "raises IndexError if there is no group with the provided name" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.bytebegin("y") + }.should raise_error(IndexError, "undefined group name reference: y") + end + end + + context "when passed a Symbol argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin(:a).should == 1 + match_data.bytebegin(:b).should == 3 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.bytebegin(:a).should == 1 + match_data.bytebegin(:b).should == 4 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.bytebegin(:a).should == 1 + match_data.bytebegin(:b).should == 4 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.bytebegin(:a).should == 3 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin(:æ).should == 1 + end + + it "raises IndexError if there is no group with the provided name" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.bytebegin(:y) + }.should raise_error(IndexError, "undefined group name reference: y") + end + end + end +end diff --git a/spec/ruby/core/matchdata/byteend_spec.rb b/spec/ruby/core/matchdata/byteend_spec.rb new file mode 100644 index 0000000000..98015e287d --- /dev/null +++ b/spec/ruby/core/matchdata/byteend_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.4" do + describe "MatchData#byteend" do + context "when passed an integer argument" do + it "returns the byte-based offset of the end of the nth element" do + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.byteend(0).should == 7 + match_data.byteend(2).should == 3 + end + + it "returns nil when the nth match isn't found" do + match_data = /something is( not)? (right)/.match("something is right") + match_data.byteend(1).should be_nil + end + + it "returns the byte-based offset for multi-byte strings" do + match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") + match_data.byteend(0).should == 8 + match_data.byteend(2).should == 4 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi-byte strings with unicode regexp" do + match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") + match_data.byteend(0).should == 8 + match_data.byteend(2).should == 4 + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.byteend(obj).should == 3 + end + end + + context "when passed a String argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend("a").should == 2 + match_data.byteend("b").should == 6 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.byteend("a").should == 3 + match_data.byteend("b").should == 7 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.byteend("a").should == 3 + match_data.byteend("b").should == 7 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.byteend("a").should == 6 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend("æ").should == 2 + end + end + + context "when passed a Symbol argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend(:a).should == 2 + match_data.byteend(:b).should == 6 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.byteend(:a).should == 3 + match_data.byteend(:b).should == 7 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.byteend(:a).should == 3 + match_data.byteend(:b).should == 7 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.byteend(:a).should == 6 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend(:æ).should == 2 + end + end + end +end diff --git a/spec/ruby/core/matchdata/offset_spec.rb b/spec/ruby/core/matchdata/offset_spec.rb index 1ccb54b7a7..a03d58aad1 100644 --- a/spec/ruby/core/matchdata/offset_spec.rb +++ b/spec/ruby/core/matchdata/offset_spec.rb @@ -1,30 +1,102 @@ -# -*- encoding: utf-8 -*- - require_relative '../../spec_helper' describe "MatchData#offset" do - it "returns a two element array with the begin and end of the nth match" do - match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") - match_data.offset(0).should == [1, 7] - match_data.offset(4).should == [6, 7] + it "returns beginning and ending character offset of whole matched substring for 0 element" do + m = /(.)(.)(\d+)(\d)/.match("THX1138.") + m.offset(0).should == [1, 7] + end + + it "returns beginning and ending character offset of n-th match, all the subsequent elements are capturing groups" do + m = /(.)(.)(\d+)(\d)/.match("THX1138.") + + m.offset(2).should == [2, 3] + m.offset(3).should == [3, 6] + m.offset(4).should == [6, 7] + end + + it "accepts String as a reference to a named capture" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.offset("f").should == [0, 3] + m.offset("b").should == [3, 6] + end + + it "accepts Symbol as a reference to a named capture" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.offset(:f).should == [0, 3] + m.offset(:b).should == [3, 6] end - it "returns [nil, nil] when the nth match isn't found" do - match_data = /something is( not)? (right)/.match("something is right") - match_data.offset(1).should == [nil, nil] + it "returns [nil, nil] if a capturing group is optional and doesn't match" do + m = /(?<x>q..)?/.match("foobarbaz") + + m.offset("x").should == [nil, nil] + m.offset(1).should == [nil, nil] end - it "returns the offset for multi byte strings" do - match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") - match_data.offset(0).should == [1, 7] - match_data.offset(4).should == [6, 7] + it "returns correct beginning and ending character offset for multi-byte strings" do + m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") + + m.offset(1).should == [1, 2] + m.offset(3).should == [2, 3] end not_supported_on :opal do - it "returns the offset for multi byte strings with unicode regexp" do - match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") - match_data.offset(0).should == [1, 7] - match_data.offset(4).should == [6, 7] + it "returns correct character offset for multi-byte strings with unicode regexp" do + m = /\A\u3042(.)(.)?(.)\z/u.match("\u3042\u3043\u3044") + + m.offset(1).should == [1, 2] + m.offset(3).should == [2, 3] end end + + it "returns [nil, nil] if a capturing group is optional and doesn't match for multi-byte string" do + m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") + + m.offset(2).should == [nil, nil] + end + + it "converts argument into integer if is not String nor Symbol" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + obj = Object.new + def obj.to_int; 2; end + + m.offset(1r).should == [0, 3] + m.offset(1.1).should == [0, 3] + m.offset(obj).should == [3, 6] + end + + it "raises IndexError if there is no group with the provided name" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.offset("y") + }.should raise_error(IndexError, "undefined group name reference: y") + + -> { + m.offset(:y) + }.should raise_error(IndexError, "undefined group name reference: y") + end + + it "raises IndexError if index is out of bounds" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.offset(-1) + }.should raise_error(IndexError, "index -1 out of matches") + + -> { + m.offset(3) + }.should raise_error(IndexError, "index 3 out of matches") + end + + it "raises TypeError if can't convert argument into Integer" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.offset([]) + }.should raise_error(TypeError, "no implicit conversion of Array into Integer") + end end diff --git a/spec/ruby/core/module/const_added_spec.rb b/spec/ruby/core/module/const_added_spec.rb index 739be3ead8..90cd36551a 100644 --- a/spec/ruby/core/module/const_added_spec.rb +++ b/spec/ruby/core/module/const_added_spec.rb @@ -117,6 +117,7 @@ describe "Module#const_added" do end ScratchPad.recorded.should == [:A, :B] + ModuleSpecs::ConstAddedSpecs.send :remove_const, :NamedModule end it "is called when a new class is defined under self" do @@ -158,6 +159,7 @@ describe "Module#const_added" do end ScratchPad.recorded.should == [:A, :B] + ModuleSpecs::ConstAddedSpecs.send :remove_const, :NamedModuleB end it "is called when an autoload is defined" do diff --git a/spec/ruby/core/module/const_source_location_spec.rb b/spec/ruby/core/module/const_source_location_spec.rb index 06b3b215c2..96649ea10b 100644 --- a/spec/ruby/core/module/const_source_location_spec.rb +++ b/spec/ruby/core/module/const_source_location_spec.rb @@ -245,6 +245,14 @@ describe "Module#const_source_location" do @line = __LINE__ - 1 end + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + end + it 'returns the autoload location while not resolved' do ConstantSpecs.const_source_location('CSL_CONST1').should == [__FILE__, @line] end @@ -265,6 +273,8 @@ describe "Module#const_source_location" do ConstantSpecs.const_source_location(:ConstSource).should == autoload_location ConstantSpecs::ConstSource::LOCATION.should == ConstantSpecs.const_source_location(:ConstSource) ConstantSpecs::BEFORE_DEFINE_LOCATION.should == autoload_location + ConstantSpecs.send :remove_const, :ConstSource + ConstantSpecs.send :remove_const, :BEFORE_DEFINE_LOCATION end end end diff --git a/spec/ruby/core/module/name_spec.rb b/spec/ruby/core/module/name_spec.rb index fd28ee0a33..d3318e1645 100644 --- a/spec/ruby/core/module/name_spec.rb +++ b/spec/ruby/core/module/name_spec.rb @@ -190,6 +190,7 @@ describe "Module#name" do ScratchPad.recorded.should.one?(/#<Module.+>::A$/) ScratchPad.recorded.should.one?(/#<Module.+>::A::B$/) + ModuleSpecs::NameSpecs.send :remove_const, :NamedModule end it "returns a frozen String" do diff --git a/spec/ruby/core/module/set_temporary_name_spec.rb b/spec/ruby/core/module/set_temporary_name_spec.rb index 12c1c214dd..624b02c90e 100644 --- a/spec/ruby/core/module/set_temporary_name_spec.rb +++ b/spec/ruby/core/module/set_temporary_name_spec.rb @@ -86,6 +86,7 @@ ruby_version_is "3.3" do ModuleSpecs::SetTemporaryNameSpec::M = m m::N.name.should == "ModuleSpecs::SetTemporaryNameSpec::M::N" + ModuleSpecs::SetTemporaryNameSpec.send :remove_const, :M end it "can update the name when assigned to a constant" do diff --git a/spec/ruby/core/random/new_spec.rb b/spec/ruby/core/random/new_spec.rb index 90e2a9d6f2..69210cef03 100644 --- a/spec/ruby/core/random/new_spec.rb +++ b/spec/ruby/core/random/new_spec.rb @@ -11,7 +11,7 @@ describe "Random.new" do it "returns Random instances initialized with different seeds" do first = Random.new second = Random.new - (0..20).map { first.rand } .should_not == (0..20).map { second.rand } + (0..20).map { first.rand }.should_not == (0..20).map { second.rand } end it "accepts an Integer seed value as an argument" do diff --git a/spec/ruby/language/regexp/encoding_spec.rb b/spec/ruby/language/regexp/encoding_spec.rb index 898b6d4ff7..ceb9cf823a 100644 --- a/spec/ruby/language/regexp/encoding_spec.rb +++ b/spec/ruby/language/regexp/encoding_spec.rb @@ -39,7 +39,11 @@ describe "Regexps with encoding modifiers" do end it "warns when using /n with a match string with non-ASCII characters and an encoding other than ASCII-8BIT" do - -> { /./n.match("\303\251".dup.force_encoding('utf-8')) }.should complain(%r{historical binary regexp match /.../n against UTF-8 string}) + -> { + eval <<~RUBY + /./n.match("\303\251".dup.force_encoding('utf-8')) + RUBY + }.should complain(%r{historical binary regexp match /.../n against UTF-8 string}) end it 'uses US-ASCII as /n encoding if all chars are 7-bit' do diff --git a/spec/ruby/language/rescue_spec.rb b/spec/ruby/language/rescue_spec.rb index 79571d689f..6be3bfd023 100644 --- a/spec/ruby/language/rescue_spec.rb +++ b/spec/ruby/language/rescue_spec.rb @@ -136,10 +136,14 @@ describe "The rescue keyword" do it 'captures successfully at the top-level' do ScratchPad.record [] + loaded_features = $".dup + begin + require_relative 'fixtures/rescue/top_level' - require_relative 'fixtures/rescue/top_level' - - ScratchPad.recorded.should == ["message"] + ScratchPad.recorded.should == ["message"] + ensure + $".replace loaded_features + end end end diff --git a/spec/ruby/library/net-http/HTTPServerException_spec.rb b/spec/ruby/library/net-http/HTTPServerException_spec.rb index 5e0a833fee..020d3cce85 100644 --- a/spec/ruby/library/net-http/HTTPServerException_spec.rb +++ b/spec/ruby/library/net-http/HTTPServerException_spec.rb @@ -3,10 +3,10 @@ require 'net/http' describe "Net::HTTPServerException" do it "is a subclass of Net::ProtoServerError and is warned as deprecated" do - -> { Net::HTTPServerException.should < Net::ProtoServerError }.should complain(/warning: constant Net::HTTPServerException is deprecated/) + -> { eval("Net::HTTPServerException").should < Net::ProtoServerError }.should complain(/warning: constant Net::HTTPServerException is deprecated/) end it "includes the Net::HTTPExceptions module and is warned as deprecated" do - -> { Net::HTTPServerException.should < Net::HTTPExceptions }.should complain(/warning: constant Net::HTTPServerException is deprecated/) + -> { eval("Net::HTTPServerException").should < Net::HTTPExceptions }.should complain(/warning: constant Net::HTTPServerException is deprecated/) end end diff --git a/spec/ruby/library/tempfile/callback_spec.rb b/spec/ruby/library/tempfile/callback_spec.rb deleted file mode 100644 index c0b1518326..0000000000 --- a/spec/ruby/library/tempfile/callback_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -require_relative '../../spec_helper' -require 'tempfile' - -describe "Tempfile.callback" do - it "needs to be reviewed for spec completeness" -end diff --git a/spec/ruby/library/tempfile/create_spec.rb b/spec/ruby/library/tempfile/create_spec.rb new file mode 100644 index 0000000000..74c48bf32a --- /dev/null +++ b/spec/ruby/library/tempfile/create_spec.rb @@ -0,0 +1,176 @@ +require_relative '../../spec_helper' +require 'tempfile' + +describe "Tempfile.create" do + after :each do + if @tempfile + @tempfile.close + File.unlink(@tempfile.path) if File.file?(@tempfile.path) + end + end + + it "returns a new, open regular File instance placed in tmpdir" do + @tempfile = Tempfile.create + # Unlike Tempfile.open this returns a true File, + # but `.should be_an_instance_of(File)` would be true either way. + @tempfile.instance_of?(File).should be_true + + @tempfile.should_not.closed? + File.file?(@tempfile.path).should be_true + + @tempfile.path.should.start_with?(Dir.tmpdir) + @tempfile.path.should_not == "#{Dir.tmpdir}/" + end + + it "returns file in w+ mode" do + @tempfile = Tempfile.create + @tempfile << "Test!\nMore test!" + @tempfile.rewind + @tempfile.read.should == "Test!\nMore test!" + + # Not "a+" mode, which would write at the end of the file. + @tempfile.rewind + @tempfile.print "Trust" + @tempfile.rewind + @tempfile.read.should == "Trust\nMore test!" + end + + platform_is_not :windows do + it "returns a private, readable and writable file" do + @tempfile = Tempfile.create + stat = @tempfile.stat + stat.should.readable? + stat.should.writable? + stat.should_not.executable? + stat.should_not.world_readable? + stat.should_not.world_writable? + end + end + + platform_is :windows do + it "returns a public, readable and writable file" do + @tempfile = Tempfile.create + stat = @tempfile.stat + stat.should.readable? + stat.should.writable? + stat.should_not.executable? + stat.should.world_readable? + stat.should.world_writable? + end + end + + context "when called with a block" do + it "returns the value of the block" do + value = Tempfile.create do |tempfile| + tempfile << "Test!" + "return" + end + value.should == "return" + end + + it "closes and unlinks file after block execution" do + Tempfile.create do |tempfile| + @tempfile = tempfile + @tempfile.should_not.closed? + File.exist?(@tempfile.path).should be_true + end + + @tempfile.should.closed? + File.exist?(@tempfile.path).should be_false + end + end + + context "when called with a single positional argument" do + it "uses a String as a prefix for the filename" do + @tempfile = Tempfile.create("create_spec") + @tempfile.path.should.start_with?("#{Dir.tmpdir}/create_spec") + @tempfile.path.should_not == "#{Dir.tmpdir}/create_spec" + end + + it "uses an array of one String as a prefix for the filename" do + @tempfile = Tempfile.create(["create_spec"]) + @tempfile.path.should.start_with?("#{Dir.tmpdir}/create_spec") + @tempfile.path.should_not == "#{Dir.tmpdir}/create_spec" + end + + it "uses an array of two Strings as a prefix and suffix for the filename" do + @tempfile = Tempfile.create(["create_spec", ".temp"]) + @tempfile.path.should.start_with?("#{Dir.tmpdir}/create_spec") + @tempfile.path.should.end_with?(".temp") + end + + it "ignores excessive array elements after the first two" do + @tempfile = Tempfile.create(["create_spec", ".temp", :".txt"]) + @tempfile.path.should.start_with?("#{Dir.tmpdir}/create_spec") + @tempfile.path.should.end_with?(".temp") + end + + it "raises ArgumentError if passed something else than a String or an array of Strings" do + -> { Tempfile.create(:create_spec) }.should raise_error(ArgumentError, "unexpected prefix: :create_spec") + -> { Tempfile.create([:create_spec]) }.should raise_error(ArgumentError, "unexpected prefix: :create_spec") + -> { Tempfile.create(["create_spec", :temp]) }.should raise_error(ArgumentError, "unexpected suffix: :temp") + end + end + + context "when called with a second positional argument" do + it "uses it as a directory for the tempfile" do + @tempfile = Tempfile.create("create_spec", "./") + @tempfile.path.should.start_with?("./create_spec") + end + + it "raises TypeError if argument can not be converted to a String" do + -> { Tempfile.create("create_spec", :temp) }.should raise_error(TypeError, "no implicit conversion of Symbol into String") + end + end + + context "when called with a mode option" do + it "ORs it with the default mode, forcing it to be readable and writable" do + @tempfile = Tempfile.create(mode: File::RDONLY) + @tempfile.puts "test" + @tempfile.rewind + @tempfile.read.should == "test\n" + end + + it "raises NoMethodError if passed a String mode" do + -> { Tempfile.create(mode: "wb") }.should raise_error(NoMethodError, /undefined method ['`]|' for .+String/) + end + end + + ruby_version_is "3.4" do + context "when called with anonymous: true" do + it "returns an already unlinked File without a proper path" do + @tempfile = Tempfile.create(anonymous: true) + @tempfile.should_not.closed? + @tempfile.path.should == "#{Dir.tmpdir}/" + File.file?(@tempfile.path).should be_false + end + + it "unlinks file before calling the block" do + Tempfile.create(anonymous: true) do |tempfile| + @tempfile = tempfile + @tempfile.should_not.closed? + @tempfile.path.should == "#{Dir.tmpdir}/" + File.file?(@tempfile.path).should be_false + end + @tempfile.should.closed? + end + end + + context "when called with anonymous: false" do + it "returns a usual File with a path" do + @tempfile = Tempfile.create(anonymous: false) + @tempfile.should_not.closed? + @tempfile.path.should.start_with?(Dir.tmpdir) + File.file?(@tempfile.path).should be_true + end + end + end + + context "when called with other options" do + it "passes them along to File.open" do + @tempfile = Tempfile.create(encoding: "IBM037:IBM037", binmode: true) + @tempfile.external_encoding.should == Encoding.find("IBM037") + @tempfile.binmode?.should be_true + end + end +end diff --git a/spec/ruby/optional/capi/encoding_spec.rb b/spec/ruby/optional/capi/encoding_spec.rb index 0c3c98a5c0..c14983c7ea 100644 --- a/spec/ruby/optional/capi/encoding_spec.rb +++ b/spec/ruby/optional/capi/encoding_spec.rb @@ -724,14 +724,16 @@ describe "C-API Encoding function" do end describe "rb_define_dummy_encoding" do + run = 0 + it "defines the dummy encoding" do - @s.rb_define_dummy_encoding("FOO") - enc = Encoding.find("FOO") + @s.rb_define_dummy_encoding("FOO#{run += 1}") + enc = Encoding.find("FOO#{run}") enc.should.dummy? end it "returns the index of the dummy encoding" do - index = @s.rb_define_dummy_encoding("BAR") + index = @s.rb_define_dummy_encoding("BAR#{run += 1}") index.should == Encoding.list.size - 1 end diff --git a/spec/ruby/optional/capi/ext/struct_spec.c b/spec/ruby/optional/capi/ext/struct_spec.c index 413249e828..756cfca8dd 100644 --- a/spec/ruby/optional/capi/ext/struct_spec.c +++ b/spec/ruby/optional/capi/ext/struct_spec.c @@ -62,6 +62,10 @@ static VALUE struct_spec_rb_struct_size(VALUE self, VALUE st) { return rb_struct_size(st); } +static VALUE struct_spec_rb_struct_initialize(VALUE self, VALUE st, VALUE values) { + return rb_struct_initialize(st, values); +} + #if defined(RUBY_VERSION_IS_3_3) /* Only allow setting three attributes, should be sufficient for testing. */ static VALUE struct_spec_rb_data_define(VALUE self, VALUE superclass, @@ -90,6 +94,7 @@ void Init_struct_spec(void) { rb_define_method(cls, "rb_struct_define_under", struct_spec_rb_struct_define_under, 5); rb_define_method(cls, "rb_struct_new", struct_spec_rb_struct_new, 4); rb_define_method(cls, "rb_struct_size", struct_spec_rb_struct_size, 1); + rb_define_method(cls, "rb_struct_initialize", struct_spec_rb_struct_initialize, 2); #if defined(RUBY_VERSION_IS_3_3) rb_define_method(cls, "rb_data_define", struct_spec_rb_data_define, 4); #endif diff --git a/spec/ruby/optional/capi/globals_spec.rb b/spec/ruby/optional/capi/globals_spec.rb index 48677620bc..4657293e15 100644 --- a/spec/ruby/optional/capi/globals_spec.rb +++ b/spec/ruby/optional/capi/globals_spec.rb @@ -41,14 +41,19 @@ describe "CApiGlobalSpecs" do @f.sb_get_global_value.should == "XYZ" end + run = 0 + it "rb_define_readonly_variable should define a new readonly global variable" do + name = "ro_gvar#{run += 1}" + eval <<~RUBY # Check the gvar doesn't exist and ensure rb_gv_get doesn't implicitly declare the gvar, # otherwise the rb_define_readonly_variable call will conflict. - suppress_warning { @f.sb_gv_get("ro_gvar") } .should == nil + suppress_warning { @f.sb_gv_get("#{name}") }.should == nil - @f.rb_define_readonly_variable("ro_gvar", 15) - $ro_gvar.should == 15 - -> { $ro_gvar = 10 }.should raise_error(NameError) + @f.rb_define_readonly_variable("#{name}", 15) + $#{name}.should == 15 + -> { $#{name} = 10 }.should raise_error(NameError) + RUBY end it "rb_define_hooked_variable should define a C hooked global variable" do diff --git a/spec/ruby/optional/capi/module_spec.rb b/spec/ruby/optional/capi/module_spec.rb index b7684e566b..af39ec0192 100644 --- a/spec/ruby/optional/capi/module_spec.rb +++ b/spec/ruby/optional/capi/module_spec.rb @@ -38,7 +38,7 @@ describe "CApiModule" do CApiModuleSpecs::C.const_set(:_INVALID, 1) }.should raise_error(NameError, /wrong constant name/) - @m.rb_const_set(CApiModuleSpecs::C, :_INVALID, 2) + suppress_warning { @m.rb_const_set(CApiModuleSpecs::C, :_INVALID, 2) } @m.rb_const_get(CApiModuleSpecs::C, :_INVALID).should == 2 # Ruby-level should still not allow access diff --git a/spec/ruby/optional/capi/struct_spec.rb b/spec/ruby/optional/capi/struct_spec.rb index 474c397956..cc8d7f932e 100644 --- a/spec/ruby/optional/capi/struct_spec.rb +++ b/spec/ruby/optional/capi/struct_spec.rb @@ -208,20 +208,48 @@ describe "C-API Struct function" do @s.rb_struct_size(@struct).should == 3 end end + + describe "rb_struct_initialize" do + it "sets all members" do + @s.rb_struct_initialize(@struct, [1, 2, 3]).should == nil + @struct.a.should == 1 + @struct.b.should == 2 + @struct.c.should == 3 + end + + it "does not freeze the Struct instance" do + @s.rb_struct_initialize(@struct, [1, 2, 3]).should == nil + @struct.should_not.frozen? + @s.rb_struct_initialize(@struct, [4, 5, 6]).should == nil + @struct.a.should == 4 + @struct.b.should == 5 + @struct.c.should == 6 + end + + it "raises ArgumentError if too many values" do + -> { @s.rb_struct_initialize(@struct, [1, 2, 3, 4]) }.should raise_error(ArgumentError, "struct size differs") + end + + it "treats missing values as nil" do + @s.rb_struct_initialize(@struct, [1, 2]).should == nil + @struct.a.should == 1 + @struct.b.should == 2 + @struct.c.should == nil + end + end end ruby_version_is "3.3" do describe "C-API Data function" do - before :each do + before :all do @s = CApiStructSpecs.new + @klass = @s.rb_data_define(nil, "a", "b", "c") end describe "rb_data_define" do it "returns a subclass of Data class when passed nil as the first argument" do - klass = @s.rb_data_define(nil, "a", "b", "c") - - klass.should.is_a? Class - klass.superclass.should == Data + @klass.should.is_a? Class + @klass.superclass.should == Data end it "returns a subclass of a class when passed as the first argument" do @@ -233,8 +261,7 @@ ruby_version_is "3.3" do end it "creates readers for the members" do - klass = @s.rb_data_define(nil, "a", "b", "c") - obj = klass.new(1, 2, 3) + obj = @klass.new(1, 2, 3) obj.a.should == 1 obj.b.should == 2 @@ -242,8 +269,7 @@ ruby_version_is "3.3" do end it "returns the member names as Symbols" do - klass = @s.rb_data_define(nil, "a", "b", "c") - obj = klass.new(0, 0, 0) + obj = @klass.new(0, 0, 0) obj.members.should == [:a, :b, :c] end @@ -256,5 +282,35 @@ ruby_version_is "3.3" do -> { @s.rb_data_define([], "a", "b", "c") }.should raise_error(TypeError, "wrong argument type Array (expected Class)") end end + + describe "rb_struct_initialize" do + it "sets all members for a Data instance" do + data = @klass.allocate + @s.rb_struct_initialize(data, [1, 2, 3]).should == nil + data.a.should == 1 + data.b.should == 2 + data.c.should == 3 + end + + it "freezes the Data instance" do + data = @klass.allocate + @s.rb_struct_initialize(data, [1, 2, 3]).should == nil + data.should.frozen? + -> { @s.rb_struct_initialize(data, [1, 2, 3]) }.should raise_error(FrozenError) + end + + it "raises ArgumentError if too many values" do + data = @klass.allocate + -> { @s.rb_struct_initialize(data, [1, 2, 3, 4]) }.should raise_error(ArgumentError, "struct size differs") + end + + it "treats missing values as nil" do + data = @klass.allocate + @s.rb_struct_initialize(data, [1, 2]).should == nil + data.a.should == 1 + data.b.should == 2 + data.c.should == nil + end + end end end |
