diff options
Diffstat (limited to 'spec/ruby/core/hash')
86 files changed, 4114 insertions, 0 deletions
diff --git a/spec/ruby/core/hash/allocate_spec.rb b/spec/ruby/core/hash/allocate_spec.rb new file mode 100644 index 0000000000..93420de866 --- /dev/null +++ b/spec/ruby/core/hash/allocate_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Hash.allocate" do + it "returns an instance of Hash" do + hsh = Hash.allocate + hsh.should be_an_instance_of(Hash) + end + + it "returns a fully-formed instance of Hash" do + hsh = Hash.allocate + hsh.size.should == 0 + hsh[:a] = 1 + hsh.should == { a: 1 } + end +end diff --git a/spec/ruby/core/hash/any_spec.rb b/spec/ruby/core/hash/any_spec.rb new file mode 100644 index 0000000000..c26dfabde6 --- /dev/null +++ b/spec/ruby/core/hash/any_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "Hash#any?" do + describe 'with no block given' do + it "checks if there are any members of a Hash" do + empty_hash = {} + empty_hash.should_not.any? + + hash_with_members = { 'key' => 'value' } + hash_with_members.should.any? + end + end + + describe 'with a block given' do + it 'is false if the hash is empty' do + empty_hash = {} + empty_hash.any? {|k,v| 1 == 1 }.should == false + end + + it 'is true if the block returns true for any member of the hash' do + hash_with_members = { 'a' => false, 'b' => false, 'c' => true, 'd' => false } + hash_with_members.any? {|k,v| v == true}.should == true + end + + it 'is false if the block returns false for all members of the hash' do + hash_with_members = { 'a' => false, 'b' => false, 'c' => true, 'd' => false } + hash_with_members.any? {|k,v| v == 42}.should == false + end + end +end diff --git a/spec/ruby/core/hash/assoc_spec.rb b/spec/ruby/core/hash/assoc_spec.rb new file mode 100644 index 0000000000..62b2a11b30 --- /dev/null +++ b/spec/ruby/core/hash/assoc_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "Hash#assoc" do + before :each do + @h = {apple: :green, orange: :orange, grape: :green, banana: :yellow} + end + + it "returns an Array if the argument is == to a key of the Hash" do + @h.assoc(:apple).should be_an_instance_of(Array) + end + + it "returns a 2-element Array if the argument is == to a key of the Hash" do + @h.assoc(:grape).size.should == 2 + end + + it "sets the first element of the Array to the located key" do + @h.assoc(:banana).first.should == :banana + end + + it "sets the last element of the Array to the value of the located key" do + @h.assoc(:banana).last.should == :yellow + end + + it "only returns the first matching key-value pair for identity hashes" do + # Avoid literal String keys since string literals can be frozen and interned e.g. with --enable-frozen-string-literal + h = {}.compare_by_identity + k1 = 'pear'.dup + h[k1] = :red + k2 = 'pear'.dup + h[k2] = :green + h.size.should == 2 + h.keys.grep(/pear/).size.should == 2 + h.assoc('pear').should == ['pear', :red] + end + + it "uses #== to compare the argument to the keys" do + @h[1.0] = :value + 1.should == 1.0 + @h.assoc(1).should == [1.0, :value] + end + + it "returns nil if the argument is not a key of the Hash" do + @h.assoc(:green).should be_nil + end + + it "returns nil if the argument is not a key of the Hash even when there is a default" do + Hash.new(42).merge!( foo: :bar ).assoc(42).should be_nil + Hash.new{|h, k| h[k] = 42}.merge!( foo: :bar ).assoc(42).should be_nil + end +end diff --git a/spec/ruby/core/hash/clear_spec.rb b/spec/ruby/core/hash/clear_spec.rb new file mode 100644 index 0000000000..cf05e36ac9 --- /dev/null +++ b/spec/ruby/core/hash/clear_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#clear" do + it "removes all key, value pairs" do + h = { 1 => 2, 3 => 4 } + h.clear.should equal(h) + h.should == {} + end + + it "does not remove default values" do + h = Hash.new(5) + h.clear + h.default.should == 5 + + h = { "a" => 100, "b" => 200 } + h.default = "Go fish" + h.clear + h["z"].should == "Go fish" + end + + it "does not remove default procs" do + h = Hash.new { 5 } + h.clear + h.default_proc.should_not == nil + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.clear }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.clear }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/clone_spec.rb b/spec/ruby/core/hash/clone_spec.rb new file mode 100644 index 0000000000..6c96fc0c67 --- /dev/null +++ b/spec/ruby/core/hash/clone_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "Hash#clone" do + it "copies instance variable but not the objects they refer to" do + hash = { 'key' => 'value' } + + clone = hash.clone + + clone.should == hash + clone.should_not equal hash + end +end diff --git a/spec/ruby/core/hash/compact_spec.rb b/spec/ruby/core/hash/compact_spec.rb new file mode 100644 index 0000000000..13371bce43 --- /dev/null +++ b/spec/ruby/core/hash/compact_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#compact" do + before :each do + @hash = { truthy: true, false: false, nil: nil, nil => true } + @initial_pairs = @hash.dup + @compact = { truthy: true, false: false, nil => true } + end + + it "returns new object that rejects pair has nil value" do + ret = @hash.compact + ret.should_not equal(@hash) + ret.should == @compact + end + + it "keeps own pairs" do + @hash.compact + @hash.should == @initial_pairs + end + + ruby_version_is '3.3' do + it "retains the default value" do + hash = Hash.new(1) + hash.compact.default.should == 1 + hash[:a] = 1 + hash.compact.default.should == 1 + end + + it "retains the default_proc" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + hash.compact.default_proc.should == pr + hash[:a] = 1 + hash.compact.default_proc.should == pr + end + + it "retains compare_by_identity flag" do + hash = {}.compare_by_identity + hash.compact.compare_by_identity?.should == true + hash[:a] = 1 + hash.compact.compare_by_identity?.should == true + end + end +end + +describe "Hash#compact!" do + before :each do + @hash = { truthy: true, false: false, nil: nil, nil => true } + @initial_pairs = @hash.dup + @compact = { truthy: true, false: false, nil => true } + end + + it "returns self" do + @hash.compact!.should equal(@hash) + end + + it "rejects own pair has nil value" do + @hash.compact! + @hash.should == @compact + end + + context "when each pair does not have nil value" do + before :each do + @hash.compact! + end + + it "returns nil" do + @hash.compact!.should be_nil + end + end + + describe "on frozen instance" do + before :each do + @hash.freeze + end + + it "keeps pairs and raises a FrozenError" do + ->{ @hash.compact! }.should raise_error(FrozenError) + @hash.should == @initial_pairs + end + end +end diff --git a/spec/ruby/core/hash/compare_by_identity_spec.rb b/spec/ruby/core/hash/compare_by_identity_spec.rb new file mode 100644 index 0000000000..2975526a97 --- /dev/null +++ b/spec/ruby/core/hash/compare_by_identity_spec.rb @@ -0,0 +1,147 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#compare_by_identity" do + before :each do + @h = {} + @idh = {}.compare_by_identity + end + + it "causes future comparisons on the receiver to be made by identity" do + @h[[1]] = :a + @h[[1]].should == :a + @h.compare_by_identity + @h[[1].dup].should be_nil + end + + it "rehashes internally so that old keys can be looked up" do + h = {} + (1..10).each { |k| h[k] = k } + o = Object.new + def o.hash; 123; end + h[o] = 1 + h.compare_by_identity + h[o].should == 1 + end + + it "returns self" do + h = {} + h[:foo] = :bar + h.compare_by_identity.should equal h + end + + it "has no effect on an already compare_by_identity hash" do + @idh[:foo] = :bar + @idh.compare_by_identity.should equal @idh + @idh.should.compare_by_identity? + @idh[:foo].should == :bar + end + + it "uses the semantics of BasicObject#equal? to determine key identity" do + [1].should_not equal([1]) + @idh[[1]] = :c + @idh[[1]] = :d + :bar.should equal(:bar) + @idh[:bar] = :e + @idh[:bar] = :f + @idh.values.should == [:c, :d, :f] + end + + it "uses #equal? semantics, but doesn't actually call #equal? to determine identity" do + obj = mock('equal') + obj.should_not_receive(:equal?) + @idh[:foo] = :glark + @idh[obj] = :a + @idh[obj].should == :a + end + + it "does not call #hash on keys" do + key = HashSpecs::ByIdentityKey.new + @idh[key] = 1 + @idh[key].should == 1 + end + + it "regards #dup'd objects as having different identities" do + key = ['foo'] + @idh[key.dup] = :str + @idh[key].should be_nil + end + + it "regards #clone'd objects as having different identities" do + key = ['foo'] + @idh[key.clone] = :str + @idh[key].should be_nil + end + + it "regards references to the same object as having the same identity" do + o = Object.new + @h[o] = :o + @h[:a] = :a + @h[o].should == :o + end + + it "raises a FrozenError on frozen hashes" do + @h = @h.freeze + -> { @h.compare_by_identity }.should raise_error(FrozenError) + end + + # Behaviour confirmed in https://bugs.ruby-lang.org/issues/1871 + it "persists over #dups" do + @idh['foo'.dup] = :bar + @idh['foo'.dup] = :glark + @idh.dup.should == @idh + @idh.dup.size.should == @idh.size + @idh.dup.should.compare_by_identity? + end + + it "persists over #clones" do + @idh['foo'.dup] = :bar + @idh['foo'.dup] = :glark + @idh.clone.should == @idh + @idh.clone.size.should == @idh.size + @idh.dup.should.compare_by_identity? + end + + it "does not copy string keys" do + foo = 'foo' + @idh[foo] = true + @idh[foo] = true + @idh.size.should == 1 + @idh.keys.first.should equal foo + end + + # Check `#[]=` call with a String literal. + # Don't use `#+` because with `#+` it's no longer a String literal. + # + # See https://bugs.ruby-lang.org/issues/12855 + it "gives different identity for string literals" do + eval <<~RUBY + # frozen_string_literal: false + @idh['foo'] = 1 + @idh['foo'] = 2 + RUBY + @idh.values.should == [1, 2] + @idh.size.should == 2 + end +end + +describe "Hash#compare_by_identity?" do + it "returns false by default" do + h = {} + h.compare_by_identity?.should be_false + end + + it "returns true once #compare_by_identity has been invoked on self" do + h = {} + h.compare_by_identity + h.compare_by_identity?.should be_true + end + + it "returns true when called multiple times on the same ident hash" do + h = {} + h.compare_by_identity + h.compare_by_identity?.should be_true + h.compare_by_identity?.should be_true + h.compare_by_identity?.should be_true + end +end diff --git a/spec/ruby/core/hash/constructor_spec.rb b/spec/ruby/core/hash/constructor_spec.rb new file mode 100644 index 0000000000..0f97f7b40e --- /dev/null +++ b/spec/ruby/core/hash/constructor_spec.rb @@ -0,0 +1,129 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.[]" do + describe "passed zero arguments" do + it "returns an empty hash" do + Hash[].should == {} + end + end + + it "creates a Hash; values can be provided as the argument list" do + Hash[:a, 1, :b, 2].should == { a: 1, b: 2 } + Hash[].should == {} + Hash[:a, 1, :b, { c: 2 }].should == { a: 1, b: { c: 2 } } + end + + it "creates a Hash; values can be provided as one single hash" do + Hash[a: 1, b: 2].should == { a: 1, b: 2 } + Hash[{1 => 2, 3 => 4}].should == {1 => 2, 3 => 4} + Hash[{}].should == {} + end + + describe "passed an array" do + it "treats elements that are 2 element arrays as key and value" do + Hash[[[:a, :b], [:c, :d]]].should == { a: :b, c: :d } + end + + it "treats elements that are 1 element arrays as keys with value nil" do + Hash[[[:a]]].should == { a: nil } + end + end + + # #1000 #1385 + it "creates a Hash; values can be provided as a list of value-pairs in an array" do + Hash[[[:a, 1], [:b, 2]]].should == { a: 1, b: 2 } + end + + it "coerces a single argument which responds to #to_ary" do + ary = mock('to_ary') + ary.should_receive(:to_ary).and_return([[:a, :b]]) + + Hash[ary].should == { a: :b } + end + + it "raises for elements that are not arrays" do + -> { + Hash[[:a]].should == {} + }.should raise_error(ArgumentError) + -> { + Hash[[:nil]].should == {} + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for arrays of more than 2 elements" do + ->{ Hash[[[:a, :b, :c]]].should == {} }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed a list of value-invalid-pairs in an array" do + -> { + -> { + Hash[[[:a, 1], [:b], 42, [:d, 2], [:e, 2, 3], []]] + }.should complain(/ignoring wrong elements/) + }.should raise_error(ArgumentError) + end + + describe "passed a single argument which responds to #to_hash" do + it "coerces it and returns a copy" do + h = { a: :b, c: :d } + to_hash = mock('to_hash') + to_hash.should_receive(:to_hash).and_return(h) + + result = Hash[to_hash] + result.should == h + result.should_not equal(h) + end + end + + it "raises an ArgumentError when passed an odd number of arguments" do + -> { Hash[1, 2, 3] }.should raise_error(ArgumentError) + -> { Hash[1, 2, { 3 => 4 }] }.should raise_error(ArgumentError) + end + + it "calls to_hash" do + obj = mock('x') + def obj.to_hash() { 1 => 2, 3 => 4 } end + Hash[obj].should == { 1 => 2, 3 => 4 } + end + + it "returns an instance of a subclass when passed an Array" do + HashSpecs::MyHash[1,2,3,4].should be_an_instance_of(HashSpecs::MyHash) + end + + it "returns instances of subclasses" do + HashSpecs::MyHash[].should be_an_instance_of(HashSpecs::MyHash) + end + + it "returns an instance of the class it's called on" do + Hash[HashSpecs::MyHash[1, 2]].class.should == Hash + HashSpecs::MyHash[Hash[1, 2]].should be_an_instance_of(HashSpecs::MyHash) + end + + it "does not call #initialize on the subclass instance" do + HashSpecs::MyInitializerHash[Hash[1, 2]].should be_an_instance_of(HashSpecs::MyInitializerHash) + end + + 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 "does not retain the default_proc" do + hash = Hash.new { |h, k| h[k] = [] } + Hash[hash].default_proc.should be_nil + hash[:a] = 1 + Hash[hash].default_proc.should be_nil + end + + ruby_version_is '3.3' do + it "does not retain compare_by_identity flag" do + hash = { a: 1 }.compare_by_identity + Hash[hash].compare_by_identity?.should == false + + hash = {}.compare_by_identity + Hash[hash].compare_by_identity?.should == false + end + end +end diff --git a/spec/ruby/core/hash/deconstruct_keys_spec.rb b/spec/ruby/core/hash/deconstruct_keys_spec.rb new file mode 100644 index 0000000000..bbcd8932e5 --- /dev/null +++ b/spec/ruby/core/hash/deconstruct_keys_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Hash#deconstruct_keys" do + it "returns self" do + hash = {a: 1, b: 2} + + hash.deconstruct_keys([:a, :b]).should equal hash + end + + it "requires one argument" do + -> { + {a: 1}.deconstruct_keys + }.should raise_error(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/) + end + + it "ignores argument" do + hash = {a: 1, b: 2} + + hash.deconstruct_keys([:a]).should == {a: 1, b: 2} + hash.deconstruct_keys(0 ).should == {a: 1, b: 2} + hash.deconstruct_keys('' ).should == {a: 1, b: 2} + end +end diff --git a/spec/ruby/core/hash/default_proc_spec.rb b/spec/ruby/core/hash/default_proc_spec.rb new file mode 100644 index 0000000000..f4e4803632 --- /dev/null +++ b/spec/ruby/core/hash/default_proc_spec.rb @@ -0,0 +1,80 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#default_proc" do + it "returns the block passed to Hash.new" do + h = Hash.new { 'Paris' } + p = h.default_proc + p.call(1).should == 'Paris' + end + + it "returns nil if no block was passed to proc" do + {}.default_proc.should == nil + end +end + +describe "Hash#default_proc=" do + it "replaces the block passed to Hash.new" do + h = Hash.new { 'Paris' } + h.default_proc = Proc.new { 'Montreal' } + p = h.default_proc + p.call(1).should == 'Montreal' + end + + it "uses :to_proc on its argument" do + h = Hash.new { 'Paris' } + obj = mock('to_proc') + obj.should_receive(:to_proc).and_return(Proc.new { 'Montreal' }) + (h.default_proc = obj).should equal(obj) + h[:cool_city].should == 'Montreal' + end + + it "overrides the static default" do + h = Hash.new(42) + h.default_proc = Proc.new { 6 } + h.default.should be_nil + h.default_proc.call.should == 6 + end + + it "raises an error if passed stuff not convertible to procs" do + ->{{}.default_proc = 42}.should raise_error(TypeError) + end + + it "returns the passed Proc" do + new_proc = Proc.new {} + ({}.default_proc = new_proc).should equal(new_proc) + end + + it "clears the default proc if passed nil" do + h = Hash.new { 'Paris' } + h.default_proc = nil + h.default_proc.should == nil + h[:city].should == nil + end + + it "returns nil if passed nil" do + ({}.default_proc = nil).should be_nil + end + + it "accepts a lambda with an arity of 2" do + h = {} + -> do + h.default_proc = -> a, b { } + end.should_not raise_error(TypeError) + end + + it "raises a TypeError if passed a lambda with an arity other than 2" do + h = {} + -> do + h.default_proc = -> a { } + end.should raise_error(TypeError) + -> do + h.default_proc = -> a, b, c { } + end.should raise_error(TypeError) + end + + it "raises a FrozenError if self is frozen" do + -> { {}.freeze.default_proc = Proc.new {} }.should raise_error(FrozenError) + -> { {}.freeze.default_proc = nil }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/default_spec.rb b/spec/ruby/core/hash/default_spec.rb new file mode 100644 index 0000000000..d8b62ea196 --- /dev/null +++ b/spec/ruby/core/hash/default_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#default" do + it "returns the default value" do + h = Hash.new(5) + h.default.should == 5 + h.default(4).should == 5 + {}.default.should == nil + {}.default(4).should == nil + end + + it "uses the default proc to compute a default value, passing given key" do + h = Hash.new { |*args| args } + h.default(nil).should == [h, nil] + h.default(5).should == [h, 5] + end + + it "calls default proc with nil arg if passed a default proc but no arg" do + h = Hash.new { |*args| args } + h.default.should == nil + end +end + +describe "Hash#default=" do + it "sets the default value" do + h = {} + h.default = 99 + h.default.should == 99 + end + + it "unsets the default proc" do + [99, nil, -> { 6 }].each do |default| + h = Hash.new { 5 } + h.default_proc.should_not == nil + h.default = default + h.default.should == default + h.default_proc.should == nil + end + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.default = nil }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.default = nil }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/delete_if_spec.rb b/spec/ruby/core/hash/delete_if_spec.rb new file mode 100644 index 0000000000..c9e670ffc3 --- /dev/null +++ b/spec/ruby/core/hash/delete_if_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#delete_if" do + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.delete_if { |*args| all_args << args } + all_args.sort.should == [[1, 2], [3, 4]] + end + + it "removes every entry for which block is true and returns self" do + h = { a: 1, b: 2, c: 3, d: 4 } + h.delete_if { |k,v| v % 2 == 1 }.should equal(h) + h.should == { b: 2, d: 4 } + end + + it "removes all entries if the block is true" do + h = { a: 1, b: 2, c: 3 } + h.delete_if { |k,v| true }.should equal(h) + h.should == {} + end + + it "processes entries with the same order as each()" do + h = { a: 1, b: 2, c: 3, d: 4 } + + each_pairs = [] + delete_pairs = [] + + h.each_pair { |k,v| each_pairs << [k, v] } + h.delete_if { |k,v| delete_pairs << [k,v] } + + each_pairs.should == delete_pairs + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.delete_if { false } }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.delete_if { true } }.should raise_error(FrozenError) + end + + it_behaves_like :hash_iteration_no_block, :delete_if + it_behaves_like :enumeratorized_with_origin_size, :delete_if, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/delete_spec.rb b/spec/ruby/core/hash/delete_spec.rb new file mode 100644 index 0000000000..3e3479c69c --- /dev/null +++ b/spec/ruby/core/hash/delete_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#delete" do + it "removes the entry and returns the deleted value" do + h = { a: 5, b: 2 } + h.delete(:b).should == 2 + h.should == { a: 5 } + end + + it "calls supplied block if the key is not found" do + { a: 1, b: 10, c: 100 }.delete(:d) { 5 }.should == 5 + Hash.new(:default).delete(:d) { 5 }.should == 5 + Hash.new { :default }.delete(:d) { 5 }.should == 5 + end + + it "returns nil if the key is not found when no block is given" do + { a: 1, b: 10, c: 100 }.delete(:d).should == nil + Hash.new(:default).delete(:d).should == nil + Hash.new { :default }.delete(:d).should == nil + end + + # MRI explicitly implements this behavior + it "allows removing a key while iterating" do + h = { a: 1, b: 2 } + visited = [] + h.each_pair { |k, v| + visited << k + h.delete(k) + } + visited.should == [:a, :b] + h.should == {} + end + + it "allows removing a key while iterating for big hashes" do + h = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, + k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, + u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 } + visited = [] + h.each_pair { |k, v| + visited << k + h.delete(k) + } + visited.should == [:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, + :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z] + h.should == {} + end + + it "accepts keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + { key => 5 }.delete(key).should == 5 + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.delete("foo") }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.delete("foo") }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/dig_spec.rb b/spec/ruby/core/hash/dig_spec.rb new file mode 100644 index 0000000000..aa0ecadd2f --- /dev/null +++ b/spec/ruby/core/hash/dig_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +describe "Hash#dig" do + + it "returns #[] with one arg" do + h = { 0 => false, a: 1 } + h.dig(:a).should == 1 + h.dig(0).should be_false + h.dig(1).should be_nil + end + + it "returns the nested value specified by the sequence of keys" do + h = { foo: { bar: { baz: 1 } } } + h.dig(:foo, :bar, :baz).should == 1 + h.dig(:foo, :bar, :nope).should be_nil + h.dig(:foo, :baz).should be_nil + h.dig(:bar, :baz, :foo).should be_nil + end + + it "returns the nested value specified if the sequence includes an index" do + h = { foo: [1, 2, 3] } + h.dig(:foo, 2).should == 3 + end + + it "returns nil if any intermediate step is nil" do + h = { foo: { bar: { baz: 1 } } } + h.dig(:foo, :zot, :xyz).should == nil + end + + it "raises an ArgumentError if no arguments provided" do + -> { { the: 'borg' }.dig() }.should raise_error(ArgumentError) + end + + it "handles type-mixed deep digging" do + h = {} + h[:foo] = [ { bar: [ 1 ] }, [ obj = Object.new, 'str' ] ] + def obj.dig(*args); [ 42 ] end + + h.dig(:foo, 0, :bar).should == [ 1 ] + h.dig(:foo, 0, :bar, 0).should == 1 + h.dig(:foo, 1, 1).should == 'str' + # MRI does not recurse values returned from `obj.dig` + h.dig(:foo, 1, 0, 0).should == [ 42 ] + h.dig(:foo, 1, 0, 0, 10).should == [ 42 ] + end + + it "raises TypeError if an intermediate element does not respond to #dig" do + h = {} + h[:foo] = [ { bar: [ 1 ] }, [ nil, 'str' ] ] + -> { h.dig(:foo, 0, :bar, 0, 0) }.should raise_error(TypeError) + -> { h.dig(:foo, 1, 1, 0) }.should raise_error(TypeError) + end + + it "calls #dig on the result of #[] with the remaining arguments" do + h = { foo: { bar: { baz: 42 } } } + h[:foo].should_receive(:dig).with(:bar, :baz).and_return(42) + h.dig(:foo, :bar, :baz).should == 42 + end + + it "respects Hash's default" do + default = {bar: 42} + h = Hash.new(default) + h.dig(:foo).should equal default + h.dig(:foo, :bar).should == 42 + end +end diff --git a/spec/ruby/core/hash/each_key_spec.rb b/spec/ruby/core/hash/each_key_spec.rb new file mode 100644 index 0000000000..c84dd696d5 --- /dev/null +++ b/spec/ruby/core/hash/each_key_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each_key" do + it "calls block once for each key, passing key" do + r = {} + h = { 1 => -1, 2 => -2, 3 => -3, 4 => -4 } + h.each_key { |k| r[k] = k }.should equal(h) + r.should == { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } + end + + it "processes keys in the same order as keys()" do + keys = [] + h = { 1 => -1, 2 => -2, 3 => -3, 4 => -4 } + h.each_key { |k| keys << k } + keys.should == h.keys + end + + it_behaves_like :hash_iteration_no_block, :each_key + it_behaves_like :enumeratorized_with_origin_size, :each_key, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/each_pair_spec.rb b/spec/ruby/core/hash/each_pair_spec.rb new file mode 100644 index 0000000000..eb6656681d --- /dev/null +++ b/spec/ruby/core/hash/each_pair_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative 'shared/each' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each_pair" do + it_behaves_like :hash_each, :each_pair + it_behaves_like :hash_iteration_no_block, :each_pair + it_behaves_like :enumeratorized_with_origin_size, :each_pair, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/each_spec.rb b/spec/ruby/core/hash/each_spec.rb new file mode 100644 index 0000000000..f0de0bdee5 --- /dev/null +++ b/spec/ruby/core/hash/each_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative 'shared/each' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each" do + it_behaves_like :hash_each, :each + it_behaves_like :hash_iteration_no_block, :each + it_behaves_like :enumeratorized_with_origin_size, :each, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/each_value_spec.rb b/spec/ruby/core/hash/each_value_spec.rb new file mode 100644 index 0000000000..19b076730d --- /dev/null +++ b/spec/ruby/core/hash/each_value_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each_value" do + it "calls block once for each key, passing value" do + r = [] + h = { a: -5, b: -3, c: -2, d: -1, e: -1 } + h.each_value { |v| r << v }.should equal(h) + r.sort.should == [-5, -3, -2, -1, -1] + end + + it "processes values in the same order as values()" do + values = [] + h = { a: -5, b: -3, c: -2, d: -1, e: -1 } + h.each_value { |v| values << v } + values.should == h.values + end + + it_behaves_like :hash_iteration_no_block, :each_value + it_behaves_like :enumeratorized_with_origin_size, :each_value, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/element_reference_spec.rb b/spec/ruby/core/hash/element_reference_spec.rb new file mode 100644 index 0000000000..d5859cb342 --- /dev/null +++ b/spec/ruby/core/hash/element_reference_spec.rb @@ -0,0 +1,134 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#[]" do + it "returns the value for key" do + obj = mock('x') + h = { 1 => 2, 3 => 4, "foo" => "bar", obj => obj, [] => "baz" } + h[1].should == 2 + h[3].should == 4 + h["foo"].should == "bar" + h[obj].should == obj + h[[]].should == "baz" + end + + it "returns nil as default value" do + { 0 => 0 }[5].should == nil + end + + it "returns the default (immediate) value for missing keys" do + h = Hash.new(5) + h[:a].should == 5 + h[:a] = 0 + h[:a].should == 0 + h[:b].should == 5 + end + + it "calls subclass implementations of default" do + h = HashSpecs::DefaultHash.new + h[:nothing].should == 100 + end + + it "does not create copies of the immediate default value" do + str = +"foo" + h = Hash.new(str) + a = h[:a] + b = h[:b] + a << "bar" + + a.should equal(b) + a.should == "foobar" + b.should == "foobar" + end + + it "returns the default (dynamic) value for missing keys" do + h = Hash.new { |hsh, k| k.kind_of?(Numeric) ? hsh[k] = k + 2 : hsh[k] = k } + h[1].should == 3 + h['this'].should == 'this' + h.should == { 1 => 3, 'this' => 'this' } + + i = 0 + h = Hash.new { |hsh, key| i += 1 } + h[:foo].should == 1 + h[:foo].should == 2 + h[:bar].should == 3 + end + + it "does not return default values for keys with nil values" do + h = Hash.new(5) + h[:a] = nil + h[:a].should == nil + + h = Hash.new { 5 } + h[:a] = nil + h[:a].should == nil + end + + it "compares keys with eql? semantics" do + { 1.0 => "x" }[1].should == nil + { 1.0 => "x" }[1.0].should == "x" + { 1 => "x" }[1.0].should == nil + { 1 => "x" }[1].should == "x" + end + + it "compares key via hash" do + x = mock('0') + x.should_receive(:hash).and_return(0) + + h = {} + # 1.9 only calls #hash if the hash had at least one entry beforehand. + h[:foo] = :bar + h[x].should == nil + end + + it "does not compare keys with different #hash values via #eql?" do + x = mock('x') + x.should_not_receive(:eql?) + x.stub!(:hash).and_return(0) + + y = mock('y') + y.should_not_receive(:eql?) + y.stub!(:hash).and_return(1) + + { y => 1 }[x].should == nil + end + + it "compares keys with the same #hash value via #eql?" do + x = mock('x') + x.should_receive(:eql?).and_return(true) + x.stub!(:hash).and_return(42) + + y = mock('y') + y.should_not_receive(:eql?) + y.stub!(:hash).and_return(42) + + { y => 1 }[x].should == 1 + end + + it "finds a value via an identical key even when its #eql? isn't reflexive" do + x = mock('x') + x.should_receive(:hash).at_least(1).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + { x => :x }[x].should == :x + end + + it "supports keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + { key => 42 }[key].should == 42 + end + + it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do + code = <<-EOC + load '#{fixture __FILE__, "name.rb"}' + hash = { true => 42, false => 42, 1 => 42, 2.0 => 42, "hello" => 42, :ok => 42 } + [true, false, 1, 2.0, "hello", :ok].each do |value| + raise "incorrect value" unless hash[value] == 42 + end + puts "Ok." + EOC + result = ruby_exe(code, args: "2>&1") + result.should == "Ok.\n" + end + +end diff --git a/spec/ruby/core/hash/element_set_spec.rb b/spec/ruby/core/hash/element_set_spec.rb new file mode 100644 index 0000000000..67c5a04d73 --- /dev/null +++ b/spec/ruby/core/hash/element_set_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/store' + +describe "Hash#[]=" do + it_behaves_like :hash_store, :[]= +end diff --git a/spec/ruby/core/hash/empty_spec.rb b/spec/ruby/core/hash/empty_spec.rb new file mode 100644 index 0000000000..881e1cc34b --- /dev/null +++ b/spec/ruby/core/hash/empty_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#empty?" do + it "returns true if the hash has no entries" do + {}.should.empty? + { 1 => 1 }.should_not.empty? + end + + it "returns true if the hash has no entries and has a default value" do + Hash.new(5).should.empty? + Hash.new { 5 }.should.empty? + Hash.new { |hsh, k| hsh[k] = k }.should.empty? + end +end diff --git a/spec/ruby/core/hash/eql_spec.rb b/spec/ruby/core/hash/eql_spec.rb new file mode 100644 index 0000000000..9013e12ffd --- /dev/null +++ b/spec/ruby/core/hash/eql_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/eql' + +describe "Hash#eql?" do + it_behaves_like :hash_eql, :eql? + it_behaves_like :hash_eql_additional, :eql? + it_behaves_like :hash_eql_additional_more, :eql? +end diff --git a/spec/ruby/core/hash/equal_value_spec.rb b/spec/ruby/core/hash/equal_value_spec.rb new file mode 100644 index 0000000000..ae13a42679 --- /dev/null +++ b/spec/ruby/core/hash/equal_value_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/eql' + +describe "Hash#==" do + it_behaves_like :hash_eql, :== + it_behaves_like :hash_eql_additional, :== + it_behaves_like :hash_eql_additional_more, :== + + it "compares values with == semantics" do + l_val = mock("left") + r_val = mock("right") + + l_val.should_receive(:==).with(r_val).and_return(true) + + ({ 1 => l_val } == { 1 => r_val }).should be_true + end +end diff --git a/spec/ruby/core/hash/except_spec.rb b/spec/ruby/core/hash/except_spec.rb new file mode 100644 index 0000000000..026e454b13 --- /dev/null +++ b/spec/ruby/core/hash/except_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "Hash#except" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns a new duplicate hash without arguments" do + ret = @hash.except + ret.should_not equal(@hash) + ret.should == @hash + end + + it "returns a hash without the requested subset" do + @hash.except(:c, :a).should == { b: 2 } + end + + it "ignores keys not present in the original hash" do + @hash.except(:a, :chunky_bacon).should == { b: 2, c: 3 } + end + + 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/fetch_spec.rb b/spec/ruby/core/hash/fetch_spec.rb new file mode 100644 index 0000000000..6e0d207224 --- /dev/null +++ b/spec/ruby/core/hash/fetch_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/hash/key_error' + +describe "Hash#fetch" do + context "when the key is not found" do + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new({ a: 5 }) + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, {} + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new { 5 } + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new(5) + + it "formats the object with #inspect in the KeyError message" do + -> { + {}.fetch('foo') + }.should raise_error(KeyError, 'key not found: "foo"') + end + end + + it "returns the value for key" do + { a: 1, b: -1 }.fetch(:b).should == -1 + end + + it "returns default if key is not found when passed a default" do + {}.fetch(:a, nil).should == nil + {}.fetch(:a, 'not here!').should == "not here!" + { a: nil }.fetch(:a, 'not here!').should == nil + end + + it "returns value of block if key is not found when passed a block" do + {}.fetch('a') { |k| k + '!' }.should == "a!" + end + + it "gives precedence to the default block over the default argument when passed both" do + -> { + @result = {}.fetch(9, :foo) { |i| i * i } + }.should complain(/block supersedes default value argument/) + @result.should == 81 + end + + it "raises an ArgumentError when not passed one or two arguments" do + -> { {}.fetch() }.should raise_error(ArgumentError) + -> { {}.fetch(1, 2, 3) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/hash/fetch_values_spec.rb b/spec/ruby/core/hash/fetch_values_spec.rb new file mode 100644 index 0000000000..0cd48565af --- /dev/null +++ b/spec/ruby/core/hash/fetch_values_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/hash/key_error' + +describe "Hash#fetch_values" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + describe "with matched keys" do + it "returns the values for keys" do + @hash.fetch_values(:a).should == [1] + @hash.fetch_values(:a, :c).should == [1, 3] + end + + it "returns the values for keys ordered in the order of the requested keys" do + @hash.fetch_values(:c, :a).should == [3, 1] + end + end + + describe "with unmatched keys" do + it_behaves_like :key_error, -> obj, key { obj.fetch_values(key) }, Hash.new({ a: 5 }) + + it "returns the default value from block" do + @hash.fetch_values(:z) { |key| "`#{key}' is not found" }.should == ["`z' is not found"] + @hash.fetch_values(:a, :z) { |key| "`#{key}' is not found" }.should == [1, "`z' is not found"] + end + end + + describe "without keys" do + it "returns an empty Array" do + @hash.fetch_values.should == [] + end + end +end diff --git a/spec/ruby/core/hash/filter_spec.rb b/spec/ruby/core/hash/filter_spec.rb new file mode 100644 index 0000000000..7dabe44984 --- /dev/null +++ b/spec/ruby/core/hash/filter_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Hash#filter" do + it_behaves_like :hash_select, :filter +end + +describe "Hash#filter!" do + it_behaves_like :hash_select!, :filter! +end diff --git a/spec/ruby/core/hash/fixtures/classes.rb b/spec/ruby/core/hash/fixtures/classes.rb new file mode 100644 index 0000000000..ae907aaff6 --- /dev/null +++ b/spec/ruby/core/hash/fixtures/classes.rb @@ -0,0 +1,75 @@ +module HashSpecs + class MyHash < Hash; end + + class MyInitializerHash < Hash + + def initialize + raise "Constructor called" + end + + end + + class NewHash < Hash + def initialize(*args) + args.each_with_index do |val, index| + self[index] = val + end + end + end + + class SubHashSettingInInitialize < Hash + def initialize(*args, &block) + self[:foo] = :bar + super(*args, &block) + end + end + + class DefaultHash < Hash + def default(key) + 100 + end + end + + class ToHashHash < Hash + def to_hash + { "to_hash" => "was", "called!" => "duh." } + end + end + + class KeyWithPrivateHash + private :hash + end + + class ByIdentityKey + def hash + fail("#hash should not be called on compare_by_identity Hash") + end + end + + class ByValueKey + attr_reader :n + def initialize(n) + @n = n + end + + def hash + n + end + + def eql? other + ByValueKey === other and @n == other.n + end + end + + def self.empty_frozen_hash + @empty ||= {} + @empty.freeze + @empty + end + + def self.frozen_hash + @hash ||= { 1 => 2, 3 => 4 } + @hash.freeze + @hash + end +end diff --git a/spec/ruby/core/hash/fixtures/name.rb b/spec/ruby/core/hash/fixtures/name.rb new file mode 100644 index 0000000000..b203bf6ae4 --- /dev/null +++ b/spec/ruby/core/hash/fixtures/name.rb @@ -0,0 +1,30 @@ +class TrueClass + def hash + raise "TrueClass#hash should not be called" + end +end +class FalseClass + def hash + raise "FalseClass#hash should not be called" + end +end +class Integer + def hash + raise "Integer#hash should not be called" + end +end +class Float + def hash + raise "Float#hash should not be called" + end +end +class String + def hash + raise "String#hash should not be called" + end +end +class Symbol + def hash + raise "Symbol#hash should not be called" + end +end diff --git a/spec/ruby/core/hash/flatten_spec.rb b/spec/ruby/core/hash/flatten_spec.rb new file mode 100644 index 0000000000..825da15bfc --- /dev/null +++ b/spec/ruby/core/hash/flatten_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "Hash#flatten" do + + before :each do + @h = {plato: :greek, + witgenstein: [:austrian, :british], + russell: :welsh} + end + + it "returns an Array" do + {}.flatten.should be_an_instance_of(Array) + end + + it "returns an empty Array for an empty Hash" do + {}.flatten.should == [] + end + + it "sets each even index of the Array to a key of the Hash" do + a = @h.flatten + a[0].should == :plato + a[2].should == :witgenstein + a[4].should == :russell + end + + it "sets each odd index of the Array to the value corresponding to the previous element" do + a = @h.flatten + a[1].should == :greek + a[3].should == [:austrian, :british] + a[5].should == :welsh + end + + it "does not recursively flatten Array values when called without arguments" do + a = @h.flatten + a[3].should == [:austrian, :british] + end + + it "does not recursively flatten Hash values when called without arguments" do + @h[:russell] = {born: :wales, influenced_by: :mill } + a = @h.flatten + a[5].should_not == {born: :wales, influenced_by: :mill }.flatten + end + + it "recursively flattens Array values when called with an argument >= 2" do + a = @h.flatten(2) + a[3].should == :austrian + a[4].should == :british + end + + it "recursively flattens Array values to the given depth" do + @h[:russell] = [[:born, :wales], [:influenced_by, :mill]] + a = @h.flatten(2) + a[6].should == [:born, :wales] + a[7].should == [:influenced_by, :mill] + end + + it "raises a TypeError if given a non-Integer argument" do + -> do + @h.flatten(Object.new) + end.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/hash/gt_spec.rb b/spec/ruby/core/hash/gt_spec.rb new file mode 100644 index 0000000000..cd541d4d83 --- /dev/null +++ b/spec/ruby/core/hash/gt_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/greater_than' + +describe "Hash#>" do + it_behaves_like :hash_comparison, :> + it_behaves_like :hash_greater_than, :> + + it "returns false if both hashes are identical" do + h = { a: 1, b: 2 } + (h > h).should be_false + end +end + +describe "Hash#>" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is smaller than argument" do + (@hash > @bigger).should == false + (@unrelated > @bigger).should == false + end + + it "returns false when receiver size is the same as argument" do + (@hash > @hash).should == false + (@hash > @unrelated).should == false + (@unrelated > @hash).should == false + end + + it "returns true when argument is a subset of receiver" do + (@bigger > @hash).should == true + end + + it "returns false when keys match but values don't" do + (@hash > @similar).should == false + (@similar > @hash).should == false + end +end diff --git a/spec/ruby/core/hash/gte_spec.rb b/spec/ruby/core/hash/gte_spec.rb new file mode 100644 index 0000000000..99b89e7217 --- /dev/null +++ b/spec/ruby/core/hash/gte_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/greater_than' + +describe "Hash#>=" do + it_behaves_like :hash_comparison, :>= + it_behaves_like :hash_greater_than, :>= + + it "returns true if both hashes are identical" do + h = { a: 1, b: 2 } + (h >= h).should be_true + end +end + +describe "Hash#>=" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is smaller than argument" do + (@hash >= @bigger).should == false + (@unrelated >= @bigger).should == false + end + + it "returns false when argument is not a subset or not equals to receiver" do + (@hash >= @unrelated).should == false + (@unrelated >= @hash).should == false + end + + it "returns true when argument is a subset of receiver or equals to receiver" do + (@bigger >= @hash).should == true + (@hash >= @hash).should == true + end + + it "returns false when keys match but values don't" do + (@hash >= @similar).should == false + (@similar >= @hash).should == false + end +end diff --git a/spec/ruby/core/hash/has_key_spec.rb b/spec/ruby/core/hash/has_key_spec.rb new file mode 100644 index 0000000000..4af53579e5 --- /dev/null +++ b/spec/ruby/core/hash/has_key_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' + +describe "Hash#has_key?" do + it_behaves_like :hash_key_p, :has_key? +end diff --git a/spec/ruby/core/hash/has_value_spec.rb b/spec/ruby/core/hash/has_value_spec.rb new file mode 100644 index 0000000000..39f1627fd3 --- /dev/null +++ b/spec/ruby/core/hash/has_value_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/value' + +describe "Hash#has_value?" do + it_behaves_like :hash_value_p, :has_value? +end diff --git a/spec/ruby/core/hash/hash_spec.rb b/spec/ruby/core/hash/hash_spec.rb new file mode 100644 index 0000000000..cd67f7a652 --- /dev/null +++ b/spec/ruby/core/hash/hash_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "Hash" do + it "includes Enumerable" do + Hash.include?(Enumerable).should == true + end +end + +describe "Hash#hash" do + it "returns a value which doesn't depend on the hash order" do + { 0=>2, 11=>1 }.hash.should == { 11=>1, 0=>2 }.hash + end + + it "returns a value in which element values do not cancel each other out" do + { a: 2, b: 2 }.hash.should_not == { a: 7, b: 7 }.hash + end + + it "returns a value in which element keys and values do not cancel each other out" do + { :a => :a }.hash.should_not == { :b => :b }.hash + end + + it "generates a hash for recursive hash structures" do + h = {} + h[:a] = h + (h.hash == h[:a].hash).should == true + end + + it "returns the same hash for recursive hashes" do + h = {} ; h[:x] = h + h.hash.should == {x: h}.hash + h.hash.should == {x: {x: h}}.hash + # This is because h.eql?(x: h) + # Remember that if two objects are eql? + # then the need to have the same hash. + # Check the Hash#eql? specs! + end + + it "returns the same hash for recursive hashes through arrays" do + h = {} ; rec = [h] ; h[:x] = rec + h.hash.should == {x: rec}.hash + h.hash.should == {x: [h]}.hash + # Like above, because h.eql?(x: [h]) + end + + it "allows omitting values" do + a = 1 + b = 2 + + {a:, b:}.should == { a: 1, b: 2 } + end +end diff --git a/spec/ruby/core/hash/include_spec.rb b/spec/ruby/core/hash/include_spec.rb new file mode 100644 index 0000000000..f3959dc589 --- /dev/null +++ b/spec/ruby/core/hash/include_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' + +describe "Hash#include?" do + it_behaves_like :hash_key_p, :include? +end diff --git a/spec/ruby/core/hash/initialize_spec.rb b/spec/ruby/core/hash/initialize_spec.rb new file mode 100644 index 0000000000..d13496ba3b --- /dev/null +++ b/spec/ruby/core/hash/initialize_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#initialize" do + it "is private" do + Hash.should have_private_instance_method("initialize") + end + + it "can be used to reset default_proc" do + h = { "foo" => 1, "bar" => 2 } + h.default_proc.should == nil + h.send(:initialize) { |_, k| k * 2 } + h.default_proc.should_not == nil + h["a"].should == "aa" + end + + it "can be used to reset the default value" do + h = {} + h.default = 42 + h.default.should == 42 + h.send(:initialize, 1) + h.default.should == 1 + h.send(:initialize) + h.default.should == nil + end + + it "receives the arguments passed to Hash#new" do + HashSpecs::NewHash.new(:one, :two)[0].should == :one + HashSpecs::NewHash.new(:one, :two)[1].should == :two + end + + it "does not change the storage, only the default value or proc" do + h = HashSpecs::SubHashSettingInInitialize.new + h.to_a.should == [[:foo, :bar]] + + h = HashSpecs::SubHashSettingInInitialize.new(:default) + h.to_a.should == [[:foo, :bar]] + + h = HashSpecs::SubHashSettingInInitialize.new { :default_block } + h.to_a.should == [[:foo, :bar]] + end + + it "returns self" do + h = Hash.new + h.send(:initialize).should equal(h) + end + + it "raises a FrozenError if called on a frozen instance" do + block = -> { HashSpecs.frozen_hash.instance_eval { initialize() }} + block.should raise_error(FrozenError) + + block = -> { HashSpecs.frozen_hash.instance_eval { initialize(nil) } } + block.should raise_error(FrozenError) + + block = -> { HashSpecs.frozen_hash.instance_eval { initialize(5) } } + block.should raise_error(FrozenError) + + block = -> { HashSpecs.frozen_hash.instance_eval { initialize { 5 } } } + block.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/inspect_spec.rb b/spec/ruby/core/hash/inspect_spec.rb new file mode 100644 index 0000000000..f41ebb70a6 --- /dev/null +++ b/spec/ruby/core/hash/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "Hash#inspect" do + it_behaves_like :hash_to_s, :inspect +end diff --git a/spec/ruby/core/hash/invert_spec.rb b/spec/ruby/core/hash/invert_spec.rb new file mode 100644 index 0000000000..c06e15ff7c --- /dev/null +++ b/spec/ruby/core/hash/invert_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#invert" do + it "returns a new hash where keys are values and vice versa" do + { 1 => 'a', 2 => 'b', 3 => 'c' }.invert.should == + { 'a' => 1, 'b' => 2, 'c' => 3 } + end + + it "handles collisions by overriding with the key coming later in keys()" do + h = { a: 1, b: 1 } + override_key = h.keys.last + h.invert[1].should == override_key + end + + it "compares new keys with eql? semantics" do + h = { a: 1.0, b: 1 } + i = h.invert + i[1.0].should == :a + i[1].should == :b + end + + it "does not return subclass instances for subclasses" 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/keep_if_spec.rb b/spec/ruby/core/hash/keep_if_spec.rb new file mode 100644 index 0000000000..d50d969467 --- /dev/null +++ b/spec/ruby/core/hash/keep_if_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#keep_if" do + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.keep_if { |*args| all_args << args } + all_args.should == [[1, 2], [3, 4]] + end + + it "keeps every entry for which block is true and returns self" do + h = { a: 1, b: 2, c: 3, d: 4 } + h.keep_if { |k,v| v % 2 == 0 }.should equal(h) + h.should == { b: 2, d: 4 } + end + + it "removes all entries if the block is false" do + h = { a: 1, b: 2, c: 3 } + h.keep_if { |k,v| false }.should equal(h) + h.should == {} + end + + it "returns self even if unmodified" do + h = { 1 => 2, 3 => 4 } + h.keep_if { true }.should equal(h) + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.keep_if { true } }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.keep_if { false } }.should raise_error(FrozenError) + end + + it_behaves_like :hash_iteration_no_block, :keep_if + it_behaves_like :enumeratorized_with_origin_size, :keep_if, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/key_spec.rb b/spec/ruby/core/hash/key_spec.rb new file mode 100644 index 0000000000..73eecbc98e --- /dev/null +++ b/spec/ruby/core/hash/key_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' +require_relative 'shared/index' + +describe "Hash#key?" do + it_behaves_like :hash_key_p, :key? +end + +describe "Hash#key" do + it_behaves_like :hash_index, :key +end diff --git a/spec/ruby/core/hash/keys_spec.rb b/spec/ruby/core/hash/keys_spec.rb new file mode 100644 index 0000000000..9a067085e5 --- /dev/null +++ b/spec/ruby/core/hash/keys_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#keys" do + + it "returns an array with the keys in the order they were inserted" do + {}.keys.should == [] + {}.keys.should be_kind_of(Array) + Hash.new(5).keys.should == [] + Hash.new { 5 }.keys.should == [] + { 1 => 2, 4 => 8, 2 => 4 }.keys.should == [1, 4, 2] + { 1 => 2, 2 => 4, 4 => 8 }.keys.should be_kind_of(Array) + { nil => nil }.keys.should == [nil] + end + + it "uses the same order as #values" do + h = { 1 => "1", 2 => "2", 3 => "3", 4 => "4" } + + h.size.times do |i| + h[h.keys[i]].should == h.values[i] + end + end +end diff --git a/spec/ruby/core/hash/length_spec.rb b/spec/ruby/core/hash/length_spec.rb new file mode 100644 index 0000000000..d0af0945df --- /dev/null +++ b/spec/ruby/core/hash/length_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "Hash#length" do + it_behaves_like :hash_length, :length +end diff --git a/spec/ruby/core/hash/lt_spec.rb b/spec/ruby/core/hash/lt_spec.rb new file mode 100644 index 0000000000..2219615880 --- /dev/null +++ b/spec/ruby/core/hash/lt_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/less_than' + +describe "Hash#<" do + it_behaves_like :hash_comparison, :< + it_behaves_like :hash_less_than, :< + + it "returns false if both hashes are identical" do + h = { a: 1, b: 2 } + (h < h).should be_false + end +end + +describe "Hash#<" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is larger than argument" do + (@bigger < @hash).should == false + (@bigger < @unrelated).should == false + end + + it "returns false when receiver size is the same as argument" do + (@hash < @hash).should == false + (@hash < @unrelated).should == false + (@unrelated < @hash).should == false + end + + it "returns true when receiver is a subset of argument" do + (@hash < @bigger).should == true + end + + it "returns false when keys match but values don't" do + (@hash < @similar).should == false + (@similar < @hash).should == false + end +end diff --git a/spec/ruby/core/hash/lte_spec.rb b/spec/ruby/core/hash/lte_spec.rb new file mode 100644 index 0000000000..a166e5bca4 --- /dev/null +++ b/spec/ruby/core/hash/lte_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/less_than' + +describe "Hash#<=" do + it_behaves_like :hash_comparison, :<= + it_behaves_like :hash_less_than, :<= + + it "returns true if both hashes are identical" do + h = { a: 1, b: 2 } + (h <= h).should be_true + end +end + +describe "Hash#<=" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is larger than argument" do + (@bigger <= @hash).should == false + (@bigger <= @unrelated).should == false + end + + it "returns false when receiver size is the same as argument" do + (@hash <= @unrelated).should == false + (@unrelated <= @hash).should == false + end + + it "returns true when receiver is a subset of argument or equals to argument" do + (@hash <= @bigger).should == true + (@hash <= @hash).should == true + end + + it "returns false when keys match but values don't" do + (@hash <= @similar).should == false + (@similar <= @hash).should == false + end +end diff --git a/spec/ruby/core/hash/member_spec.rb b/spec/ruby/core/hash/member_spec.rb new file mode 100644 index 0000000000..37c0414559 --- /dev/null +++ b/spec/ruby/core/hash/member_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' + +describe "Hash#member?" do + it_behaves_like :hash_key_p, :member? +end diff --git a/spec/ruby/core/hash/merge_spec.rb b/spec/ruby/core/hash/merge_spec.rb new file mode 100644 index 0000000000..6710d121ef --- /dev/null +++ b/spec/ruby/core/hash/merge_spec.rb @@ -0,0 +1,123 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative 'shared/update' + +describe "Hash#merge" do + it "returns a new hash by combining self with the contents of other" do + h = { 1 => :a, 2 => :b, 3 => :c }.merge(a: 1, c: 2) + h.should == { c: 2, 1 => :a, 2 => :b, a: 1, 3 => :c } + + hash = { a: 1, b: 2 } + {}.merge(hash).should == hash + hash.merge({}).should == hash + + h = { 1 => :a, 2 => :b, 3 => :c }.merge(1 => :b) + h.should == { 1 => :b, 2 => :b, 3 => :c } + + h = { 1 => :a, 2 => :b }.merge(1 => :b, 3 => :c) + h.should == { 1 => :b, 2 => :b, 3 => :c } + end + + it "sets any duplicate key to the value of block if passed a block" do + h1 = { a: 2, b: 1, d: 5 } + h2 = { a: -2, b: 4, c: -3 } + r = h1.merge(h2) { |k,x,y| nil } + r.should == { a: nil, b: nil, c: -3, d: 5 } + + r = h1.merge(h2) { |k,x,y| "#{k}:#{x+2*y}" } + r.should == { a: "a:-2", b: "b:9", c: -3, d: 5 } + + -> { + h1.merge(h2) { |k, x, y| raise(IndexError) } + }.should raise_error(IndexError) + + r = h1.merge(h1) { |k,x,y| :x } + r.should == { a: :x, b: :x, d: :x } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2}') + obj.should_receive(:to_hash).and_return({ 1 => 2 }) + { 3 => 4 }.merge(obj).should == { 1 => 2, 3 => 4 } + end + + it "does not call to_hash on hash subclasses" do + { 3 => 4 }.merge(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } + end + + it "returns subclass instance for subclasses" do + HashSpecs::MyHash[1 => 2, 3 => 4].merge({ 1 => 2 }).should be_an_instance_of(HashSpecs::MyHash) + HashSpecs::MyHash[].merge({ 1 => 2 }).should be_an_instance_of(HashSpecs::MyHash) + + { 1 => 2, 3 => 4 }.merge(HashSpecs::MyHash[1 => 2]).class.should == Hash + {}.merge(HashSpecs::MyHash[1 => 2]).class.should == Hash + end + + it "processes entries with same order as each()" do + h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } + merge_pairs = [] + each_pairs = [] + h.each_pair { |k, v| each_pairs << [k, v] } + h.merge(h) { |k, v1, v2| merge_pairs << [k, v1] } + merge_pairs.should == each_pairs + end + + it "preserves the order of merged elements" do + h1 = { 1 => 2, 3 => 4, 5 => 6 } + h2 = { 1 => 7 } + merge_pairs = [] + h1.merge(h2).each_pair { |k, v| merge_pairs << [k, v] } + merge_pairs.should == [[1,7], [3, 4], [5, 6]] + end + + it "preserves the order of merged elements for large hashes" do + h1 = {} + h2 = {} + merge_pairs = [] + expected_pairs = [] + (1..100).each { |x| h1[x] = x; h2[101 - x] = x; expected_pairs << [x, 101 - x] } + h1.merge(h2).each_pair { |k, v| merge_pairs << [k, v] } + merge_pairs.should == expected_pairs + end + + it "accepts multiple hashes" do + result = { a: 1 }.merge({ b: 2 }, { c: 3 }, { d: 4 }) + result.should == { a: 1, b: 2, c: 3, d: 4 } + end + + it "accepts zero arguments and returns a copy of self" do + hash = { a: 1 } + merged = hash.merge + + 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 + it_behaves_like :hash_update, :merge! +end diff --git a/spec/ruby/core/hash/new_spec.rb b/spec/ruby/core/hash/new_spec.rb new file mode 100644 index 0000000000..5ae3e1f98d --- /dev/null +++ b/spec/ruby/core/hash/new_spec.rb @@ -0,0 +1,67 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.new" do + it "creates an empty Hash if passed no arguments" do + Hash.new.should == {} + Hash.new.size.should == 0 + end + + it "creates a new Hash with default object if passed a default argument" do + Hash.new(5).default.should == 5 + Hash.new({}).default.should == {} + end + + it "does not create a copy of the default argument" do + str = "foo" + Hash.new(str).default.should equal(str) + end + + it "creates a Hash with a default_proc if passed a block" do + Hash.new.default_proc.should == nil + + h = Hash.new { |x| "Answer to #{x}" } + h.default_proc.call(5).should == "Answer to 5" + h.default_proc.call("x").should == "Answer to x" + end + + it "raises an ArgumentError if more than one argument is passed" do + -> { Hash.new(5,6) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed both default argument and default block" do + -> { Hash.new(5) { 0 } }.should raise_error(ArgumentError) + -> { Hash.new(nil) { 0 } }.should raise_error(ArgumentError) + end + + ruby_version_is "3.3"..."3.4" do + it "emits a deprecation warning if keyword arguments are passed" do + -> { Hash.new(unknown: true) }.should complain( + Regexp.new(Regexp.escape("Calling Hash.new with keyword arguments is deprecated and will be removed in Ruby 3.4; use Hash.new({ key: value }) instead")) + ) + + -> { Hash.new(1, unknown: true) }.should raise_error(ArgumentError) + -> { Hash.new(unknown: true) { 0 } }.should raise_error(ArgumentError) + + Hash.new({ unknown: true }).default.should == { unknown: true } + end + end + + ruby_version_is "3.4" do + it "accepts a capacity: argument" do + Hash.new(5, capacity: 42).default.should == 5 + Hash.new(capacity: 42).default.should == nil + (Hash.new(capacity: 42) { 1 }).default_proc.should_not == nil + end + + it "ignores negative capacity" do + -> { Hash.new(capacity: -42) }.should_not raise_error + end + + it "raises an error if unknown keyword arguments are passed" do + -> { Hash.new(unknown: true) }.should raise_error(ArgumentError) + -> { Hash.new(1, unknown: true) }.should raise_error(ArgumentError) + -> { Hash.new(unknown: true) { 0 } }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/hash/rassoc_spec.rb b/spec/ruby/core/hash/rassoc_spec.rb new file mode 100644 index 0000000000..f3b2a6de20 --- /dev/null +++ b/spec/ruby/core/hash/rassoc_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "Hash#rassoc" do + before :each do + @h = {apple: :green, orange: :orange, grape: :green, banana: :yellow} + end + + it "returns an Array if the argument is a value of the Hash" do + @h.rassoc(:green).should be_an_instance_of(Array) + end + + it "returns a 2-element Array if the argument is a value of the Hash" do + @h.rassoc(:orange).size.should == 2 + end + + it "sets the first element of the Array to the key of the located value" do + @h.rassoc(:yellow).first.should == :banana + end + + it "sets the last element of the Array to the located value" do + @h.rassoc(:yellow).last.should == :yellow + end + + it "only returns the first matching key-value pair" do + @h.rassoc(:green).should == [:apple, :green] + end + + it "uses #== to compare the argument to the values" do + @h[:key] = 1.0 + 1.should == 1.0 + @h.rassoc(1).should eql [:key, 1.0] + end + + it "returns nil if the argument is not a value of the Hash" do + @h.rassoc(:banana).should be_nil + end + + it "returns nil if the argument is not a value of the Hash even when there is a default" do + Hash.new(42).merge!( foo: :bar ).rassoc(42).should be_nil + Hash.new{|h, k| h[k] = 42}.merge!( foo: :bar ).rassoc(42).should be_nil + end +end diff --git a/spec/ruby/core/hash/rehash_spec.rb b/spec/ruby/core/hash/rehash_spec.rb new file mode 100644 index 0000000000..db3e91b166 --- /dev/null +++ b/spec/ruby/core/hash/rehash_spec.rb @@ -0,0 +1,114 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#rehash" do + it "reorganizes the Hash by recomputing all key hash codes" do + k1 = Object.new + k2 = Object.new + def k1.hash; 0; end + def k2.hash; 1; end + + h = {} + h[k1] = :v1 + h[k2] = :v2 + + def k1.hash; 1; end + + # The key should no longer be found as the #hash changed. + # Hash values 0 and 1 should not conflict, even with 1-bit stored hash. + h.key?(k1).should == false + + h.keys.include?(k1).should == true + + h.rehash.should equal(h) + h.key?(k1).should == true + h[k1].should == :v1 + end + + it "calls #hash for each key" do + k1 = mock('k1') + k2 = mock('k2') + v1 = mock('v1') + v2 = mock('v2') + + v1.should_not_receive(:hash) + v2.should_not_receive(:hash) + + h = { k1 => v1, k2 => v2 } + + k1.should_receive(:hash).twice.and_return(0) + k2.should_receive(:hash).twice.and_return(0) + + h.rehash + h[k1].should == v1 + h[k2].should == v2 + end + + it "removes duplicate keys" do + a = [1,2] + b = [1] + + h = {} + h[a] = true + h[b] = true + b << 2 + h.size.should == 2 + h.keys.should == [a, b] + h.rehash + h.size.should == 1 + h.keys.should == [a] + end + + it "removes duplicate keys for large hashes" do + a = [1,2] + b = [1] + + h = {} + h[a] = true + h[b] = true + 100.times { |n| h[n] = true } + b << 2 + h.size.should == 102 + h.keys.should.include? a + h.keys.should.include? b + h.rehash + h.size.should == 101 + h.keys.should.include? a + h.keys.should_not.include? [1] + end + + it "iterates keys in insertion order" do + key = Class.new do + attr_reader :name + + def initialize(name) + @name = name + end + + def hash + 123 + end + end + + a, b, c, d = key.new('a'), key.new('b'), key.new('c'), key.new('d') + h = { a => 1, b => 2, c => 3, d => 4 } + h.size.should == 4 + + key.class_exec do + def eql?(other) + true + end + end + + h.rehash + h.size.should == 1 + k, v = h.first + k.name.should == 'a' + v.should == 4 + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.rehash }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.rehash }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/reject_spec.rb b/spec/ruby/core/hash/reject_spec.rb new file mode 100644 index 0000000000..8381fc7fc1 --- /dev/null +++ b/spec/ruby/core/hash/reject_spec.rb @@ -0,0 +1,116 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#reject" do + it "returns a new hash removing keys for which the block yields true" do + h = { 1=>false, 2=>true, 3=>false, 4=>true } + h.reject { |k,v| v }.keys.sort.should == [1,3] + end + + it "is equivalent to hsh.dup.delete_if" do + h = { a: 'a', b: 'b', c: 'd' } + h.reject { |k,v| k == 'd' }.should == (h.dup.delete_if { |k, v| k == 'd' }) + + all_args_reject = [] + all_args_delete_if = [] + h = { 1 => 2, 3 => 4 } + h.reject { |*args| all_args_reject << args } + h.delete_if { |*args| all_args_delete_if << args } + all_args_reject.should == all_args_delete_if + + h = { 1 => 2 } + # dup doesn't copy singleton methods + def h.to_a() end + h.reject { false }.to_a.should == [[1, 2]] + end + + context "with extra state" do + it "returns Hash instance for subclasses" do + HashSpecs::MyHash[1 => 2, 3 => 4].reject { false }.should be_kind_of(Hash) + HashSpecs::MyHash[1 => 2, 3 => 4].reject { true }.should be_kind_of(Hash) + end + end + + it "processes entries with the same order as reject!" do + h = { a: 1, b: 2, c: 3, d: 4 } + + reject_pairs = [] + reject_bang_pairs = [] + h.dup.reject { |*pair| reject_pairs << pair } + h.reject! { |*pair| reject_bang_pairs << pair } + + 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 + +describe "Hash#reject!" do + it "removes keys from self for which the block yields true" do + hsh = {} + (1 .. 10).each { |k| hsh[k] = (k % 2 == 0) } + hsh.reject! { |k,v| v } + hsh.keys.sort.should == [1,3,5,7,9] + end + + it "removes all entries if the block is true" do + h = { a: 1, b: 2, c: 3 } + h.reject! { |k,v| true }.should equal(h) + h.should == {} + end + + it "is equivalent to delete_if if changes are made" do + hsh = { a: 1 } + hsh.reject! { |k,v| v < 2 }.should == hsh.dup.delete_if { |k, v| v < 2 } + end + + it "returns nil if no changes were made" do + { a: 1 }.reject! { |k,v| v > 1 }.should == nil + end + + it "processes entries with the same order as delete_if" do + h = { a: 1, b: 2, c: 3, d: 4 } + + reject_bang_pairs = [] + delete_if_pairs = [] + h.dup.reject! { |*pair| reject_bang_pairs << pair } + h.dup.delete_if { |*pair| delete_if_pairs << pair } + + reject_bang_pairs.should == delete_if_pairs + end + + it "raises a FrozenError if called on a frozen instance that is modified" do + -> { HashSpecs.empty_frozen_hash.reject! { true } }.should raise_error(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that would not be modified" do + -> { HashSpecs.frozen_hash.reject! { false } }.should raise_error(FrozenError) + 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 new file mode 100644 index 0000000000..db30145e1a --- /dev/null +++ b/spec/ruby/core/hash/replace_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#replace" do + it "replaces the contents of self with other" do + h = { a: 1, b: 2 } + h.replace(c: -1, d: -2).should equal(h) + h.should == { c: -1, d: -2 } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2,3=>4}') + obj.should_receive(:to_hash).and_return({ 1 => 2, 3 => 4 }) + + h = {} + h.replace(obj) + h.should == { 1 => 2, 3 => 4 } + end + + it "calls to_hash on hash subclasses" do + h = {} + h.replace(HashSpecs::ToHashHash[1 => 2]) + h.should == { 1 => 2 } + end + + it "does not retain the default value" do + hash = Hash.new(1) + hash.replace(b: 2).default.should be_nil + end + + it "transfers the default value of an argument" do + hash = Hash.new(1) + { a: 1 }.replace(hash).default.should == 1 + end + + 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 + + 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) + end.should raise_error(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that is modified" do + -> do + HashSpecs.frozen_hash.replace(HashSpecs.empty_frozen_hash) + end.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb new file mode 100644 index 0000000000..7dbb9c0a98 --- /dev/null +++ b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.ruby2_keywords_hash?" do + it "returns false if the Hash is not a keywords Hash" do + Hash.ruby2_keywords_hash?({}).should == false + end + + it "returns true if the Hash is a keywords Hash marked by Module#ruby2_keywords" do + obj = Class.new { + ruby2_keywords def m(*args) + args.last + end + }.new + Hash.ruby2_keywords_hash?(obj.m(a: 1)).should == true + end + + it "raises TypeError for non-Hash" do + -> { Hash.ruby2_keywords_hash?(nil) }.should raise_error(TypeError) + end +end + +describe "Hash.ruby2_keywords_hash" do + it "returns a copy of a Hash and marks the copy as a keywords Hash" do + h = {a: 1}.freeze + kw = Hash.ruby2_keywords_hash(h) + Hash.ruby2_keywords_hash?(h).should == false + Hash.ruby2_keywords_hash?(kw).should == true + kw.should == h + end + + it "returns an instance of the subclass if called on an instance of a subclass of Hash" do + h = HashSpecs::MyHash.new + h[:a] = 1 + kw = Hash.ruby2_keywords_hash(h) + kw.class.should == HashSpecs::MyHash + Hash.ruby2_keywords_hash?(h).should == false + Hash.ruby2_keywords_hash?(kw).should == true + kw.should == h + end + + it "copies instance variables" do + h = {a: 1} + h.instance_variable_set(:@foo, 42) + kw = Hash.ruby2_keywords_hash(h) + kw.instance_variable_get(:@foo).should == 42 + end + + it "copies the hash internals" do + h = {a: 1} + kw = Hash.ruby2_keywords_hash(h) + h[:a] = 2 + kw[:a].should == 1 + end + + it "raises TypeError for non-Hash" do + -> { Hash.ruby2_keywords_hash(nil) }.should raise_error(TypeError) + end + + it "retains the default value" do + hash = Hash.new(1) + Hash.ruby2_keywords_hash(hash).default.should == 1 + hash[:a] = 1 + Hash.ruby2_keywords_hash(hash).default.should == 1 + end + + it "retains the default_proc" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + Hash.ruby2_keywords_hash(hash).default_proc.should == pr + hash[:a] = 1 + Hash.ruby2_keywords_hash(hash).default_proc.should == pr + end + + ruby_version_is '3.3' do + it "retains compare_by_identity_flag" do + hash = {}.compare_by_identity + Hash.ruby2_keywords_hash(hash).compare_by_identity?.should == true + hash[:a] = 1 + Hash.ruby2_keywords_hash(hash).compare_by_identity?.should == true + end + end +end diff --git a/spec/ruby/core/hash/select_spec.rb b/spec/ruby/core/hash/select_spec.rb new file mode 100644 index 0000000000..38b0180b0e --- /dev/null +++ b/spec/ruby/core/hash/select_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Hash#select" do + it_behaves_like :hash_select, :select +end + +describe "Hash#select!" do + it_behaves_like :hash_select!, :select! +end diff --git a/spec/ruby/core/hash/shared/comparison.rb b/spec/ruby/core/hash/shared/comparison.rb new file mode 100644 index 0000000000..07564e4cec --- /dev/null +++ b/spec/ruby/core/hash/shared/comparison.rb @@ -0,0 +1,15 @@ +describe :hash_comparison, shared: true do + it "raises a TypeError if the right operand is not a hash" do + -> { { a: 1 }.send(@method, 1) }.should raise_error(TypeError) + -> { { a: 1 }.send(@method, nil) }.should raise_error(TypeError) + -> { { a: 1 }.send(@method, []) }.should raise_error(TypeError) + end + + it "returns false if both hashes have the same keys but different values" do + h1 = { a: 1 } + h2 = { a: 2 } + + h1.send(@method, h2).should be_false + h2.send(@method, h1).should be_false + end +end diff --git a/spec/ruby/core/hash/shared/each.rb b/spec/ruby/core/hash/shared/each.rb new file mode 100644 index 0000000000..f9839ff58f --- /dev/null +++ b/spec/ruby/core/hash/shared/each.rb @@ -0,0 +1,105 @@ +describe :hash_each, shared: true do + + # This is inconsistent with below, MRI checks the block arity in rb_hash_each_pair() + it "yields a [[key, value]] Array for each pair to a block expecting |*args|" do + all_args = [] + { 1 => 2, 3 => 4 }.send(@method) { |*args| all_args << args } + all_args.sort.should == [[[1, 2]], [[3, 4]]] + end + + it "yields the key and value of each pair to a block expecting |key, value|" do + r = {} + h = { a: 1, b: 2, c: 3, d: 5 } + h.send(@method) { |k,v| r[k.to_s] = v.to_s }.should equal(h) + r.should == { "a" => "1", "b" => "2", "c" => "3", "d" => "5" } + end + + it "yields the key only to a block expecting |key,|" do + ary = [] + h = { "a" => 1, "b" => 2, "c" => 3 } + h.send(@method) { |k,| ary << k } + ary.sort.should == ["a", "b", "c"] + end + + it "always yields an Array of 2 elements, even when given a callable of arity 2" do + obj = Object.new + def obj.foo(key, value) + end + + -> { + { "a" => 1 }.send(@method, &obj.method(:foo)) + }.should raise_error(ArgumentError) + + -> { + { "a" => 1 }.send(@method, &-> key, value { }) + }.should raise_error(ArgumentError) + end + + it "yields an Array of 2 elements when given a callable of arity 1" do + obj = Object.new + def obj.foo(key_value) + ScratchPad << key_value + end + + ScratchPad.record([]) + { "a" => 1 }.send(@method, &obj.method(:foo)) + ScratchPad.recorded.should == [["a", 1]] + end + + it "raises an error for a Hash when an arity enforcing callable of arity >2 is passed in" do + obj = Object.new + def obj.foo(key, value, extra) + end + + -> { + { "a" => 1 }.send(@method, &obj.method(:foo)) + }.should raise_error(ArgumentError) + end + + it "uses the same order as keys() and values()" do + h = { a: 1, b: 2, c: 3, d: 5 } + keys = [] + values = [] + + h.send(@method) do |k, v| + keys << k + values << v + end + + keys.should == h.keys + values.should == h.values + end + + # Confirming the argument-splatting works from child class for both k, v and [k, v] + it "properly expands (or not) child class's 'each'-yielded args" do + cls1 = Class.new(Hash) do + attr_accessor :k_v + def each + super do |k, v| + @k_v = [k, v] + yield k, v + end + end + end + + cls2 = Class.new(Hash) do + attr_accessor :k_v + def each + super do |k, v| + @k_v = [k, v] + yield([k, v]) + end + end + end + + obj1 = cls1.new + obj1['a'] = 'b' + obj1.map {|k, v| [k, v]}.should == [['a', 'b']] + obj1.k_v.should == ['a', 'b'] + + obj2 = cls2.new + obj2['a'] = 'b' + obj2.map {|k, v| [k, v]}.should == [['a', 'b']] + obj2.k_v.should == ['a', 'b'] + end +end diff --git a/spec/ruby/core/hash/shared/eql.rb b/spec/ruby/core/hash/shared/eql.rb new file mode 100644 index 0000000000..68db49f76d --- /dev/null +++ b/spec/ruby/core/hash/shared/eql.rb @@ -0,0 +1,204 @@ +describe :hash_eql, shared: true do + it "does not compare values when keys don't match" do + value = mock('x') + value.should_not_receive(:==) + value.should_not_receive(:eql?) + { 1 => value }.send(@method, { 2 => value }).should be_false + end + + it "returns false when the numbers of keys differ without comparing any elements" do + obj = mock('x') + h = { obj => obj } + + obj.should_not_receive(:==) + obj.should_not_receive(:eql?) + + {}.send(@method, h).should be_false + h.send(@method, {}).should be_false + end + + it "first compares keys via hash" do + x = mock('x') + x.should_receive(:hash).any_number_of_times.and_return(0) + y = mock('y') + y.should_receive(:hash).any_number_of_times.and_return(0) + + { x => 1 }.send(@method, { y => 1 }).should be_false + end + + it "does not compare keys with different hash codes via eql?" do + x = mock('x') + y = mock('y') + x.should_not_receive(:eql?) + y.should_not_receive(:eql?) + + x.should_receive(:hash).any_number_of_times.and_return(0) + y.should_receive(:hash).any_number_of_times.and_return(1) + + { x => 1 }.send(@method, { y => 1 }).should be_false + end + + it "computes equality for recursive hashes" do + h = {} + h[:a] = h + h.send(@method, h[:a]).should be_true + (h == h[:a]).should be_true + end + + it "doesn't call to_hash on objects" do + mock_hash = mock("fake hash") + def mock_hash.to_hash() {} end + {}.send(@method, mock_hash).should be_false + end + + it "computes equality for complex recursive hashes" do + a, b = {}, {} + a.merge! self: a, other: b + b.merge! self: b, other: a + a.send(@method, b).should be_true # they both have the same structure! + + c = {} + c.merge! other: c, self: c + c.send(@method, a).should be_true # subtle, but they both have the same structure! + a[:delta] = c[:delta] = a + c.send(@method, a).should be_false # not quite the same structure, as a[:other][:delta] = nil + c[:delta] = 42 + c.send(@method, a).should be_false + a[:delta] = 42 + c.send(@method, a).should be_false + b[:delta] = 42 + c.send(@method, a).should be_true + end + + it "computes equality for recursive hashes & arrays" do + x, y, z = [], [], [] + a, b, c = {foo: x, bar: 42}, {foo: y, bar: 42}, {foo: z, bar: 42} + x << a + y << c + z << b + b.send(@method, c).should be_true # they clearly have the same structure! + y.send(@method, z).should be_true + a.send(@method, b).should be_true # subtle, but they both have the same structure! + x.send(@method, y).should be_true + y << x + y.send(@method, z).should be_false + z << x + y.send(@method, z).should be_true + + a[:foo], a[:bar] = a[:bar], a[:foo] + a.send(@method, b).should be_false + b[:bar] = b[:foo] + b.send(@method, c).should be_false + end +end + +describe :hash_eql_additional, shared: true do + it "compares values when keys match" do + x = mock('x') + y = mock('y') + def x.==(o) false end + def y.==(o) false end + def x.eql?(o) false end + def y.eql?(o) false end + { 1 => x }.send(@method, { 1 => y }).should be_false + + x = mock('x') + y = mock('y') + def x.==(o) true end + def y.==(o) true end + def x.eql?(o) true end + def y.eql?(o) true end + { 1 => x }.send(@method, { 1 => y }).should be_true + end + + it "compares keys with eql? semantics" do + { 1.0 => "x" }.send(@method, { 1.0 => "x" }).should be_true + { 1.0 => "x" }.send(@method, { 1.0 => "x" }).should be_true + { 1 => "x" }.send(@method, { 1.0 => "x" }).should be_false + { 1.0 => "x" }.send(@method, { 1 => "x" }).should be_false + end + + it "returns true if and only if other Hash has the same number of keys and each key-value pair matches" do + a = { a: 5 } + b = {} + a.send(@method, b).should be_false + + b[:a] = 5 + a.send(@method, b).should be_true + + not_supported_on :opal do + c = { "a" => 5 } + a.send(@method, c).should be_false + end + + c = { "A" => 5 } + a.send(@method, c).should be_false + + c = { a: 6 } + a.send(@method, c).should be_false + end + + it "does not call to_hash on hash subclasses" do + { 5 => 6 }.send(@method, HashSpecs::ToHashHash[5 => 6]).should be_true + end + + it "ignores hash class differences" do + h = { 1 => 2, 3 => 4 } + HashSpecs::MyHash[h].send(@method, h).should be_true + HashSpecs::MyHash[h].send(@method, HashSpecs::MyHash[h]).should be_true + h.send(@method, HashSpecs::MyHash[h]).should be_true + end + + # Why isn't this true of eql? too ? + it "compares keys with matching hash codes via eql?" do + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + return true if self.equal?(o) + false + end + + obj + end + + { a[0] => 1 }.send(@method, { a[1] => 1 }).should be_false + + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + true + end + + obj + end + + { a[0] => 1 }.send(@method, { a[1] => 1 }).should be_true + end + + it "compares the values in self to values in other hash" do + l_val = mock("left") + r_val = mock("right") + + l_val.should_receive(:eql?).with(r_val).and_return(true) + + { 1 => l_val }.eql?({ 1 => r_val }).should be_true + end +end + +describe :hash_eql_additional_more, shared: true do + it "returns true if other Hash has the same number of keys and each key-value pair matches, even though the default-value are not same" do + Hash.new(5).send(@method, Hash.new(1)).should be_true + Hash.new {|h, k| 1}.send(@method, Hash.new {}).should be_true + Hash.new {|h, k| 1}.send(@method, Hash.new(2)).should be_true + + d = Hash.new {|h, k| 1} + e = Hash.new {} + d[1] = 2 + e[1] = 2 + d.send(@method, e).should be_true + end +end diff --git a/spec/ruby/core/hash/shared/greater_than.rb b/spec/ruby/core/hash/shared/greater_than.rb new file mode 100644 index 0000000000..1f8b9fcfb7 --- /dev/null +++ b/spec/ruby/core/hash/shared/greater_than.rb @@ -0,0 +1,23 @@ +describe :hash_greater_than, shared: true do + before do + @h1 = { a: 1, b: 2, c: 3 } + @h2 = { a: 1, b: 2 } + end + + it "returns true if the other hash is a subset of self" do + @h1.send(@method, @h2).should be_true + end + + it "returns false if the other hash is not a subset of self" do + @h2.send(@method, @h1).should be_false + end + + it "converts the right operand to a hash before comparing" do + o = Object.new + def o.to_hash + { a: 1, b: 2 } + end + + @h1.send(@method, o).should be_true + end +end diff --git a/spec/ruby/core/hash/shared/index.rb b/spec/ruby/core/hash/shared/index.rb new file mode 100644 index 0000000000..7f6a186464 --- /dev/null +++ b/spec/ruby/core/hash/shared/index.rb @@ -0,0 +1,37 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :hash_index, shared: true do + it "returns the corresponding key for value" do + suppress_warning do # for Hash#index + { 2 => 'a', 1 => 'b' }.send(@method, 'b').should == 1 + end + end + + it "returns nil if the value is not found" do + suppress_warning do # for Hash#index + { a: -1, b: 3.14, c: 2.718 }.send(@method, 1).should be_nil + end + end + + it "doesn't return default value if the value is not found" do + suppress_warning do # for Hash#index + Hash.new(5).send(@method, 5).should be_nil + end + end + + it "compares values using ==" do + suppress_warning do # for Hash#index + { 1 => 0 }.send(@method, 0.0).should == 1 + { 1 => 0.0 }.send(@method, 0).should == 1 + end + + needle = mock('needle') + inhash = mock('inhash') + inhash.should_receive(:==).with(needle).and_return(true) + + suppress_warning do # for Hash#index + { 1 => inhash }.send(@method, needle).should == 1 + end + end +end diff --git a/spec/ruby/core/hash/shared/iteration.rb b/spec/ruby/core/hash/shared/iteration.rb new file mode 100644 index 0000000000..d27c2443f8 --- /dev/null +++ b/spec/ruby/core/hash/shared/iteration.rb @@ -0,0 +1,19 @@ +describe :hash_iteration_no_block, shared: true do + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "returns an Enumerator if called on a non-empty hash without a block" do + @hsh.send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator if called on an empty hash without a block" do + @empty.send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator if called on a frozen instance" do + @hsh.freeze + @hsh.send(@method).should be_an_instance_of(Enumerator) + end +end diff --git a/spec/ruby/core/hash/shared/key.rb b/spec/ruby/core/hash/shared/key.rb new file mode 100644 index 0000000000..17f9f81457 --- /dev/null +++ b/spec/ruby/core/hash/shared/key.rb @@ -0,0 +1,38 @@ +describe :hash_key_p, shared: true do + it "returns true if argument is a key" do + h = { a: 1, b: 2, c: 3, 4 => 0 } + h.send(@method, :a).should == true + h.send(@method, :b).should == true + h.send(@method, 2).should == false + h.send(@method, 4).should == true + + not_supported_on :opal do + h.send(@method, 'b').should == false + h.send(@method, 4.0).should == false + end + end + + it "returns true if the key's matching value was nil" do + { xyz: nil }.send(@method, :xyz).should == true + end + + it "returns true if the key's matching value was false" do + { xyz: false }.send(@method, :xyz).should == true + end + + it "returns true if the key is nil" do + { nil => 'b' }.send(@method, nil).should == true + { nil => nil }.send(@method, nil).should == true + end + + it "compares keys with the same #hash value via #eql?" do + x = mock('x') + x.stub!(:hash).and_return(42) + + y = mock('y') + y.stub!(:hash).and_return(42) + y.should_receive(:eql?).and_return(false) + + { x => nil }.send(@method, y).should == false + end +end diff --git a/spec/ruby/core/hash/shared/length.rb b/spec/ruby/core/hash/shared/length.rb new file mode 100644 index 0000000000..24f5563759 --- /dev/null +++ b/spec/ruby/core/hash/shared/length.rb @@ -0,0 +1,12 @@ +describe :hash_length, shared: true do + it "returns the number of entries" do + { a: 1, b: 'c' }.send(@method).should == 2 + h = { a: 1, b: 2 } + h[:a] = 2 + h.send(@method).should == 2 + { a: 1, b: 1, c: 1 }.send(@method).should == 3 + {}.send(@method).should == 0 + Hash.new(5).send(@method).should == 0 + Hash.new { 5 }.send(@method).should == 0 + end +end diff --git a/spec/ruby/core/hash/shared/less_than.rb b/spec/ruby/core/hash/shared/less_than.rb new file mode 100644 index 0000000000..cdc6f14546 --- /dev/null +++ b/spec/ruby/core/hash/shared/less_than.rb @@ -0,0 +1,23 @@ +describe :hash_less_than, shared: true do + before do + @h1 = { a: 1, b: 2 } + @h2 = { a: 1, b: 2, c: 3 } + end + + it "returns true if self is a subset of the other hash" do + @h1.send(@method, @h2).should be_true + end + + it "returns false if self is not a subset of the other hash" do + @h2.send(@method, @h1).should be_false + end + + it "converts the right operand to a hash before comparing" do + o = Object.new + def o.to_hash + { a: 1, b: 2, c: 3 } + end + + @h1.send(@method, o).should be_true + end +end diff --git a/spec/ruby/core/hash/shared/select.rb b/spec/ruby/core/hash/shared/select.rb new file mode 100644 index 0000000000..fbeff07330 --- /dev/null +++ b/spec/ruby/core/hash/shared/select.rb @@ -0,0 +1,112 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative '../shared/iteration' +require_relative '../../enumerable/shared/enumeratorized' + +describe :hash_select, shared: true do + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.send(@method) { |*args| all_args << args } + all_args.sort.should == [[1, 2], [3, 4]] + end + + it "returns a Hash of entries for which block is true" do + a_pairs = { 'a' => 9, 'c' => 4, 'b' => 5, 'd' => 2 }.send(@method) { |k,v| v % 2 == 0 } + a_pairs.should be_an_instance_of(Hash) + a_pairs.sort.should == [['c', 4], ['d', 2]] + end + + it "processes entries with the same order as reject" do + h = { a: 9, c: 4, b: 5, d: 2 } + + select_pairs = [] + reject_pairs = [] + h.dup.send(@method) { |*pair| select_pairs << pair } + h.reject { |*pair| reject_pairs << pair } + + select_pairs.should == reject_pairs + end + + it "returns an Enumerator when called on a non-empty hash without a block" do + @hsh.send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator when called on an empty hash without a block" 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 + @object = { 1 => 2, 3 => 4, 5 => 6 } + end + it_should_behave_like :enumeratorized_with_origin_size +end + +describe :hash_select!, shared: true do + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "is equivalent to keep_if if changes are made" do + h = { a: 2 } + h.send(@method) { |k,v| v <= 1 }.should equal h + + h = { 1 => 2, 3 => 4 } + all_args_select = [] + h.dup.send(@method) { |*args| all_args_select << args } + all_args_select.should == [[1, 2], [3, 4]] + end + + it "removes all entries if the block is false" do + h = { a: 1, b: 2, c: 3 } + h.send(@method) { |k,v| false }.should equal(h) + h.should == {} + end + + it "returns nil if no changes were made" do + { a: 1 }.send(@method) { |k,v| v <= 1 }.should == nil + end + + it "raises a FrozenError if called on an empty frozen instance" do + -> { HashSpecs.empty_frozen_hash.send(@method) { false } }.should raise_error(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that would not be modified" do + -> { HashSpecs.frozen_hash.send(@method) { true } }.should raise_error(FrozenError) + end + + it_should_behave_like :hash_iteration_no_block + + before :each do + @object = { 1 => 2, 3 => 4, 5 => 6 } + end + it_should_behave_like :enumeratorized_with_origin_size +end diff --git a/spec/ruby/core/hash/shared/store.rb b/spec/ruby/core/hash/shared/store.rb new file mode 100644 index 0000000000..72a462a42f --- /dev/null +++ b/spec/ruby/core/hash/shared/store.rb @@ -0,0 +1,115 @@ +require_relative '../fixtures/classes' + +describe :hash_store, shared: true do + it "associates the key with the value and return the value" do + h = { a: 1 } + h.send(@method, :b, 2).should == 2 + h.should == { b:2, a:1 } + end + + it "duplicates string keys using dup semantics" do + # dup doesn't copy singleton methods + key = +"foo" + def key.reverse() "bar" end + h = {} + h.send(@method, key, 0) + h.keys[0].reverse.should == "oof" + end + + it "stores unequal keys that hash to the same value" do + h = {} + k1 = ["x"] + k2 = ["y"] + # So they end up in the same bucket + k1.should_receive(:hash).and_return(0) + k2.should_receive(:hash).and_return(0) + + h.send(@method, k1, 1) + h.send(@method, k2, 2) + h.size.should == 2 + end + + it "accepts keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + h = {} + h.send(@method, key, "foo") + h[key].should == "foo" + end + + it " accepts keys with an Integer hash" do + o = mock(hash: 1 << 100) + h = {} + h[o] = 1 + h[o].should == 1 + end + + it "duplicates and freezes string keys" do + key = +"foo" + h = {} + h.send(@method, key, 0) + key << "bar" + + h.should == { "foo" => 0 } + h.keys[0].should.frozen? + end + + it "doesn't duplicate and freeze already frozen string keys" do + key = "foo".freeze + h = {} + h.send(@method, key, 0) + h.keys[0].should equal(key) + end + + it "keeps the existing key in the hash if there is a matching one" do + h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } + key1 = HashSpecs::ByValueKey.new(13) + key2 = HashSpecs::ByValueKey.new(13) + h[key1] = 41 + key_in_hash = h.keys.last + key_in_hash.should equal(key1) + h[key2] = 42 + last_key = h.keys.last + last_key.should equal(key_in_hash) + last_key.should_not equal(key2) + end + + it "keeps the existing String key in the hash if there is a matching one" do + h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } + key1 = "foo".dup + key2 = "foo".dup + key1.should_not equal(key2) + h[key1] = 41 + frozen_key = h.keys.last + frozen_key.should_not equal(key1) + h[key2] = 42 + h.keys.last.should equal(frozen_key) + h.keys.last.should_not equal(key2) + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.send(@method, 1, 2) }.should raise_error(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash.each { hash.send(@method, 1, :foo) } + hash.should == {1 => :foo, 3 => 4, 5 => 6} + end + + it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do + code = <<-EOC + load '#{fixture __FILE__, "name.rb"}' + hash = {} + [true, false, 1, 2.0, "hello", :ok].each do |value| + hash[value] = 42 + raise "incorrect value" unless hash[value] == 42 + hash[value] = 43 + raise "incorrect value" unless hash[value] == 43 + end + puts "OK" + puts hash.size + EOC + result = ruby_exe(code, args: "2>&1") + result.should == "OK\n6\n" + end +end diff --git a/spec/ruby/core/hash/shared/to_s.rb b/spec/ruby/core/hash/shared/to_s.rb new file mode 100644 index 0000000000..e116b8878b --- /dev/null +++ b/spec/ruby/core/hash/shared/to_s.rb @@ -0,0 +1,93 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :hash_to_s, shared: true do + it "returns a string representation with same order as each()" do + h = { a: [1, 2], b: -2, d: -6, nil => nil } + expected = ruby_version_is("3.4") ? "{a: [1, 2], b: -2, d: -6, nil => nil}" : "{:a=>[1, 2], :b=>-2, :d=>-6, nil=>nil}" + h.send(@method).should == expected + end + + it "calls #inspect on keys and values" do + key = mock('key') + val = mock('val') + key.should_receive(:inspect).and_return('key') + val.should_receive(:inspect).and_return('val') + expected = ruby_version_is("3.4") ? "{key => val}" : "{key=>val}" + { key => val }.send(@method).should == expected + end + + it "does not call #to_s on a String returned from #inspect" do + str = +"abc" + str.should_not_receive(:to_s) + expected = ruby_version_is("3.4") ? '{a: "abc"}' : '{:a=>"abc"}' + { a: str }.send(@method).should == expected + end + + it "calls #to_s on the object returned from #inspect if the Object isn't a String" do + obj = mock("Hash#inspect/to_s calls #to_s") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return("abc") + expected = ruby_version_is("3.4") ? "{a: abc}" : "{:a=>abc}" + { a: obj }.send(@method).should == expected + end + + it "does not call #to_str on the object returned from #inspect when it is not a String" do + obj = mock("Hash#inspect/to_s does not call #to_str") + obj.should_receive(:inspect).and_return(obj) + obj.should_not_receive(:to_str) + expected_pattern = ruby_version_is("3.4") ? /^\{a: #<MockObject:0x[0-9a-f]+>\}$/ : /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/ + { a: obj }.send(@method).should =~ expected_pattern + end + + it "does not call #to_str on the object returned from #to_s when it is not a String" do + obj = mock("Hash#inspect/to_s does not call #to_str on #to_s result") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return(obj) + obj.should_not_receive(:to_str) + expected_pattern = ruby_version_is("3.4") ? /^\{a: #<MockObject:0x[0-9a-f]+>\}$/ : /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/ + { a: obj }.send(@method).should =~ expected_pattern + end + + it "does not swallow exceptions raised by #to_s" do + obj = mock("Hash#inspect/to_s does not swallow #to_s exceptions") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_raise(Exception) + + -> { { a: obj }.send(@method) }.should raise_error(Exception) + end + + it "handles hashes with recursive values" do + x = {} + x[0] = x + expected = ruby_version_is("3.4") ? '{0 => {...}}' : '{0=>{...}}' + x.send(@method).should == expected + + x = {} + y = {} + x[0] = y + y[1] = x + expected_x = ruby_version_is("3.4") ? '{0 => {1 => {...}}}' : '{0=>{1=>{...}}}' + expected_y = ruby_version_is("3.4") ? '{1 => {0 => {...}}}' : '{1=>{0=>{...}}}' + x.send(@method).should == expected_x + y.send(@method).should == expected_y + end + + it "does not raise if inspected result is not default external encoding" do + utf_16be = mock("utf_16be") + utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) + expected = ruby_version_is("3.4") ? '{a: "utf_16be \u3042"}' : '{:a=>"utf_16be \u3042"}' + {a: utf_16be}.send(@method).should == expected + end + + it "works for keys and values whose #inspect return a frozen String" do + expected = ruby_version_is("3.4") ? "{true => false}" : "{true=>false}" + { true => false }.to_s.should == expected + end + + ruby_version_is "3.4" do + it "adds quotes to symbol keys that are not valid symbol literals" do + { "needs-quotes": 1 }.send(@method).should == '{"needs-quotes": 1}' + end + end +end diff --git a/spec/ruby/core/hash/shared/update.rb b/spec/ruby/core/hash/shared/update.rb new file mode 100644 index 0000000000..1b0eb809bf --- /dev/null +++ b/spec/ruby/core/hash/shared/update.rb @@ -0,0 +1,76 @@ +describe :hash_update, shared: true do + it "adds the entries from other, overwriting duplicate keys. Returns self" do + h = { _1: 'a', _2: '3' } + h.send(@method, _1: '9', _9: 2).should equal(h) + h.should == { _1: "9", _2: "3", _9: 2 } + end + + it "sets any duplicate key to the value of block if passed a block" do + h1 = { a: 2, b: -1 } + h2 = { a: -2, c: 1 } + h1.send(@method, h2) { |k,x,y| 3.14 }.should equal(h1) + h1.should == { c: 1, b: -1, a: 3.14 } + + h1.send(@method, h1) { nil } + h1.should == { a: nil, b: nil, c: nil } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2}') + obj.should_receive(:to_hash).and_return({ 1 => 2 }) + { 3 => 4 }.send(@method, obj).should == { 1 => 2, 3 => 4 } + end + + it "does not call to_hash on hash subclasses" do + { 3 => 4 }.send(@method, HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } + end + + it "processes entries with same order as merge()" do + h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } + merge_bang_pairs = [] + merge_pairs = [] + h.merge(h) { |*arg| merge_pairs << arg } + h.send(@method, h) { |*arg| merge_bang_pairs << arg } + merge_bang_pairs.should == merge_pairs + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> do + HashSpecs.frozen_hash.send(@method, 1 => 2) + end.should raise_error(FrozenError) + end + + it "checks frozen status before coercing an object with #to_hash" do + obj = mock("to_hash frozen") + # This is necessary because mock cleanup code cannot run on the frozen + # object. + def obj.to_hash() raise Exception, "should not receive #to_hash" end + obj.freeze + + -> { HashSpecs.frozen_hash.send(@method, obj) }.should raise_error(FrozenError) + end + + # see redmine #1571 + it "raises a FrozenError on a frozen instance that would not be modified" do + -> do + HashSpecs.frozen_hash.send(@method, HashSpecs.empty_frozen_hash) + end.should raise_error(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash2 = {1 => :foo, 3 => :bar} + hash.each { hash.send(@method, hash2) } + hash.should == {1 => :foo, 3 => :bar, 5 => 6} + end + + it "accepts multiple hashes" do + result = { a: 1 }.send(@method, { b: 2 }, { c: 3 }, { d: 4 }) + result.should == { a: 1, b: 2, c: 3, d: 4 } + end + + it "accepts zero arguments" do + hash = { a: 1 } + hash.send(@method).should eql(hash) + end +end diff --git a/spec/ruby/core/hash/shared/value.rb b/spec/ruby/core/hash/shared/value.rb new file mode 100644 index 0000000000..aac76c253e --- /dev/null +++ b/spec/ruby/core/hash/shared/value.rb @@ -0,0 +1,14 @@ +describe :hash_value_p, shared: true do + it "returns true if the value exists in the hash" do + { a: :b }.send(@method, :a).should == false + { 1 => 2 }.send(@method, 2).should == true + h = Hash.new(5) + h.send(@method, 5).should == false + h = Hash.new { 5 } + h.send(@method, 5).should == false + end + + it "uses == semantics for comparing values" do + { 5 => 2.0 }.send(@method, 2).should == true + end +end diff --git a/spec/ruby/core/hash/shared/values_at.rb b/spec/ruby/core/hash/shared/values_at.rb new file mode 100644 index 0000000000..ef3b0e8ba0 --- /dev/null +++ b/spec/ruby/core/hash/shared/values_at.rb @@ -0,0 +1,9 @@ +describe :hash_values_at, shared: true do + it "returns an array of values for the given keys" do + h = { a: 9, b: 'a', c: -10, d: nil } + h.send(@method).should be_kind_of(Array) + h.send(@method).should == [] + h.send(@method, :a, :d, :b).should be_kind_of(Array) + h.send(@method, :a, :d, :b).should == [9, nil, 'a'] + end +end diff --git a/spec/ruby/core/hash/shift_spec.rb b/spec/ruby/core/hash/shift_spec.rb new file mode 100644 index 0000000000..3f31b9864c --- /dev/null +++ b/spec/ruby/core/hash/shift_spec.rb @@ -0,0 +1,78 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#shift" do + it "removes a pair from hash and return it" do + h = { a: 1, b: 2, "c" => 3, nil => 4, [] => 5 } + h2 = h.dup + + h.size.times do |i| + r = h.shift + r.should be_kind_of(Array) + h2[r.first].should == r.last + h.size.should == h2.size - i - 1 + end + + h.should == {} + end + + # MRI explicitly implements this behavior + it "allows shifting entries while iterating" do + h = { a: 1, b: 2, c: 3 } + visited = [] + shifted = [] + h.each_pair { |k,v| + visited << k + shifted << h.shift + } + visited.should == [:a, :b, :c] + shifted.should == [[:a, 1], [:b, 2], [:c, 3]] + h.should == {} + end + + it "returns nil if the Hash is empty" do + h = {} + def h.default(key) + raise + end + h.shift.should == nil + end + + it "returns nil from an empty hash" do + {}.shift.should == nil + end + + it "returns nil for empty hashes with defaults and default procs" do + Hash.new(5).shift.should == nil + h = Hash.new { |*args| args } + h.shift.should == nil + end + + it "preserves Hash invariants when removing the last item" do + h = { :a => 1, :b => 2 } + h.shift.should == [:a, 1] + h.shift.should == [:b, 2] + h[:c] = 3 + h.should == {:c => 3} + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.shift }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.shift }.should raise_error(FrozenError) + end + + it "works when the hash is at capacity" do + # We try a wide range of sizes in hopes that this will cover all implementations' base Hash size. + results = [] + 1.upto(100) do |n| + h = {} + n.times do |i| + h[i] = i + end + h.shift + results << h.size + end + + results.should == 0.upto(99).to_a + end +end diff --git a/spec/ruby/core/hash/size_spec.rb b/spec/ruby/core/hash/size_spec.rb new file mode 100644 index 0000000000..1e8abd8d97 --- /dev/null +++ b/spec/ruby/core/hash/size_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "Hash#size" do + it_behaves_like :hash_length, :size +end diff --git a/spec/ruby/core/hash/slice_spec.rb b/spec/ruby/core/hash/slice_spec.rb new file mode 100644 index 0000000000..4fcc01f9a6 --- /dev/null +++ b/spec/ruby/core/hash/slice_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' + +describe "Hash#slice" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns a new empty hash without arguments" do + ret = @hash.slice + ret.should_not equal(@hash) + ret.should be_an_instance_of(Hash) + ret.should == {} + end + + it "returns the requested subset" do + @hash.slice(:c, :a).should == { c: 3, a: 1 } + end + + it "returns a hash ordered in the order of the requested keys" do + @hash.slice(:c, :a).keys.should == [:c, :a] + end + + it "returns only the keys of the original hash" do + @hash.slice(:a, :chunky_bacon).should == { a: 1 } + end + + it "returns a Hash instance, even on subclasses" do + klass = Class.new(Hash) + h = klass.new + h[:bar] = 12 + h[:foo] = 42 + r = h.slice(:foo) + r.should == {foo: 42} + r.class.should == Hash + end + + it "uses the regular Hash#[] method, even on subclasses that override it" do + ScratchPad.record [] + klass = Class.new(Hash) do + def [](value) + ScratchPad << :used_subclassed_operator + super + end + end + + h = klass.new + h[:bar] = 12 + h[:foo] = 42 + h.slice(:foo) + + 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/sort_spec.rb b/spec/ruby/core/hash/sort_spec.rb new file mode 100644 index 0000000000..26058c845e --- /dev/null +++ b/spec/ruby/core/hash/sort_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#sort" do + it "converts self to a nested array of [key, value] arrays and sort with Array#sort" do + { 'a' => 'b', '1' => '2', 'b' => 'a' }.sort.should == + [["1", "2"], ["a", "b"], ["b", "a"]] + end + + it "works when some of the keys are themselves arrays" do + { [1,2] => 5, [1,1] => 5 }.sort.should == [[[1,1],5], [[1,2],5]] + end + + it "uses block to sort array if passed a block" do + { 1 => 2, 2 => 9, 3 => 4 }.sort { |a,b| b <=> a }.should == [[3, 4], [2, 9], [1, 2]] + end +end diff --git a/spec/ruby/core/hash/store_spec.rb b/spec/ruby/core/hash/store_spec.rb new file mode 100644 index 0000000000..7e975380ec --- /dev/null +++ b/spec/ruby/core/hash/store_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/store' + +describe "Hash#store" do + it_behaves_like :hash_store, :store +end diff --git a/spec/ruby/core/hash/to_a_spec.rb b/spec/ruby/core/hash/to_a_spec.rb new file mode 100644 index 0000000000..5baf677929 --- /dev/null +++ b/spec/ruby/core/hash/to_a_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_a" do + it "returns a list of [key, value] pairs with same order as each()" do + h = { a: 1, 1 => :a, 3 => :b, b: 5 } + pairs = [] + + h.each_pair do |key, value| + pairs << [key, value] + end + + h.to_a.should be_kind_of(Array) + h.to_a.should == pairs + end + + it "is called for Enumerable#entries" do + h = { a: 1, 1 => :a, 3 => :b, b: 5 } + pairs = [] + + h.each_pair do |key, value| + pairs << [key, value] + end + + ent = h.entries + ent.should be_kind_of(Array) + ent.should == pairs + end +end diff --git a/spec/ruby/core/hash/to_h_spec.rb b/spec/ruby/core/hash/to_h_spec.rb new file mode 100644 index 0000000000..f84fd7b503 --- /dev/null +++ b/spec/ruby/core/hash/to_h_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_h" do + it "returns self for Hash instances" do + h = {} + h.to_h.should equal(h) + end + + describe "when called on a subclass of Hash" do + before :each do + @h = HashSpecs::MyHash.new + @h[:foo] = :bar + end + + it "returns a new Hash instance" do + @h.to_h.should be_an_instance_of(Hash) + @h.to_h.should == @h + @h[:foo].should == :bar + end + + it "retains the default" do + @h.default = 42 + @h.to_h.default.should == 42 + @h[:hello].should == 42 + end + + 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 + it "converts [key, value] pairs returned by the block to a hash" do + { a: 1, b: 2 }.to_h { |k, v| [k.to_s, v*v]}.should == { "a" => 1, "b" => 4 } + end + + it "passes to a block each pair's key and value as separate arguments" do + ScratchPad.record [] + { a: 1, b: 2 }.to_h { |k, v| ScratchPad << [k, v]; [k, v] } + ScratchPad.recorded.sort.should == [[:a, 1], [:b, 2]] + + ScratchPad.record [] + { a: 1, b: 2 }.to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [[:a, 1], [:b, 2]] + end + + it "raises ArgumentError if block returns longer or shorter array" do + -> do + { a: 1, b: 2 }.to_h { |k, v| [k.to_s, v*v, 1] } + end.should raise_error(ArgumentError, /element has wrong array length/) + + -> do + { a: 1, b: 2 }.to_h { |k, v| [k] } + end.should raise_error(ArgumentError, /element has wrong array length/) + end + + it "raises TypeError if block returns something other than Array" do + -> do + { a: 1, b: 2 }.to_h { |k, v| "not-array" } + end.should raise_error(TypeError, /wrong element type String/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + + { a: 1 }.to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + + -> 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/to_hash_spec.rb b/spec/ruby/core/hash/to_hash_spec.rb new file mode 100644 index 0000000000..f479fa1fb2 --- /dev/null +++ b/spec/ruby/core/hash/to_hash_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_hash" do + it "returns self for Hash instances" do + h = {} + h.to_hash.should equal(h) + end + + it "returns self for instances of subclasses of Hash" do + h = HashSpecs::MyHash.new + h.to_hash.should equal(h) + end +end diff --git a/spec/ruby/core/hash/to_proc_spec.rb b/spec/ruby/core/hash/to_proc_spec.rb new file mode 100644 index 0000000000..9dbc79e5eb --- /dev/null +++ b/spec/ruby/core/hash/to_proc_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_proc" do + before :each do + @key = Object.new + @value = Object.new + @hash = { @key => @value } + @default = Object.new + @unstored = Object.new + end + + it "returns an instance of Proc" do + @hash.to_proc.should be_an_instance_of Proc + end + + describe "the returned proc" do + before :each do + @proc = @hash.to_proc + end + + it "is a lambda" do + @proc.should.lambda? + end + + it "has an arity of 1" do + @proc.arity.should == 1 + end + + it "raises ArgumentError if not passed exactly one argument" do + -> { + @proc.call + }.should raise_error(ArgumentError) + + -> { + @proc.call 1, 2 + }.should raise_error(ArgumentError) + end + + context "with a stored key" do + it "returns the paired value" do + @proc.call(@key).should equal(@value) + end + end + + context "passed as a block" do + it "retrieves the hash's values" do + [@key].map(&@proc)[0].should equal(@value) + end + + context "to instance_exec" do + it "always retrieves the original hash's values" do + hash = {foo: 1, bar: 2} + proc = hash.to_proc + + hash.instance_exec(:foo, &proc).should == 1 + + hash2 = {quux: 1} + hash2.instance_exec(:foo, &proc).should == 1 + end + end + end + + context "with no stored key" do + it "returns nil" do + @proc.call(@unstored).should be_nil + end + + context "when the hash has a default value" do + before :each do + @hash.default = @default + end + + it "returns the default value" do + @proc.call(@unstored).should equal(@default) + end + end + + context "when the hash has a default proc" do + it "returns an evaluated value from the default proc" do + @hash.default_proc = -> hash, called_with { [hash.keys, called_with] } + @proc.call(@unstored).should == [[@key], @unstored] + end + end + end + + it "raises an ArgumentError when calling #call on the Proc with no arguments" do + -> { @hash.to_proc.call }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/hash/to_s_spec.rb b/spec/ruby/core/hash/to_s_spec.rb new file mode 100644 index 0000000000..e52b09962e --- /dev/null +++ b/spec/ruby/core/hash/to_s_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "Hash#to_s" do + it_behaves_like :hash_to_s, :to_s +end diff --git a/spec/ruby/core/hash/transform_keys_spec.rb b/spec/ruby/core/hash/transform_keys_spec.rb new file mode 100644 index 0000000000..e2eeab1813 --- /dev/null +++ b/spec/ruby/core/hash/transform_keys_spec.rb @@ -0,0 +1,150 @@ +require_relative '../../spec_helper' + +describe "Hash#transform_keys" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns new hash" do + ret = @hash.transform_keys(&:succ) + ret.should_not equal(@hash) + ret.should be_an_instance_of(Hash) + end + + it "sets the result as transformed keys with the given block" do + @hash.transform_keys(&:succ).should == { b: 1, c: 2, d: 3 } + end + + it "keeps last pair if new keys conflict" do + @hash.transform_keys { |_| :a }.should == { a: 3 } + end + + it "makes both hashes to share values" do + value = [1, 2, 3] + new_hash = { a: value }.transform_keys(&:upcase) + new_hash[:A].should equal(value) + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_keys + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:succ).should == { b: 1, c: 2, d: 3 } + end + end + + it "returns a Hash instance, even on subclasses" do + klass = Class.new(Hash) + h = klass.new + h[:foo] = 42 + r = h.transform_keys{|v| :"x#{v}"} + r.keys.should == [:xfoo] + r.class.should == Hash + end + + it "allows a hash argument" do + @hash.transform_keys({ a: :A, b: :B, c: :C }).should == { A: 1, B: 2, C: 3 } + end + + it "allows a partial transformation of keys when using a hash argument" do + @hash.transform_keys({ a: :A, c: :C }).should == { A: 1, b: 2, C: 3 } + end + + 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 + before :each do + @hash = { a: 1, b: 2, c: 3, d: 4 } + @initial_pairs = @hash.dup + end + + it "returns self" do + @hash.transform_keys!(&:succ).should equal(@hash) + end + + it "updates self as transformed values with the given block" do + @hash.transform_keys!(&:to_s) + @hash.should == { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 } + end + + it "prevents conflicts between new keys and old ones" do + @hash.transform_keys!(&:succ) + @hash.should == { b: 1, c: 2, d: 3, e: 4 } + end + + it "returns the processed keys and non evaluated keys if we break from the block" do + @hash.transform_keys! do |v| + break if v == :c + v.succ + end + @hash.should == { b: 1, c: 2, d: 4 } + end + + it "keeps later pair if new keys conflict" do + @hash.transform_keys! { |_| :a }.should == { a: 4 } + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_keys! + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:upcase).should == { A: 1, B: 2, C: 3, D: 4 } + end + end + + it "allows a hash argument" do + @hash.transform_keys!({ a: :A, b: :B, c: :C, d: :D }) + @hash.should == { A: 1, B: 2, C: 3, D: 4 } + end + + describe "on frozen instance" do + before :each do + @hash.freeze + end + + it "raises a FrozenError on an empty hash" do + ->{ {}.freeze.transform_keys!(&:upcase) }.should raise_error(FrozenError) + end + + it "keeps pairs and raises a FrozenError" do + ->{ @hash.transform_keys!(&:upcase) }.should raise_error(FrozenError) + @hash.should == @initial_pairs + end + + it "raises a FrozenError on hash argument" do + ->{ @hash.transform_keys!({ a: :A, b: :B, c: :C }) }.should raise_error(FrozenError) + end + + context "when no block is given" do + it "does not raise an exception" do + @hash.transform_keys!.should be_an_instance_of(Enumerator) + end + end + end +end diff --git a/spec/ruby/core/hash/transform_values_spec.rb b/spec/ruby/core/hash/transform_values_spec.rb new file mode 100644 index 0000000000..4a0ae8a5a5 --- /dev/null +++ b/spec/ruby/core/hash/transform_values_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' + +describe "Hash#transform_values" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns new hash" do + ret = @hash.transform_values(&:succ) + ret.should_not equal(@hash) + ret.should be_an_instance_of(Hash) + end + + it "sets the result as transformed values with the given block" do + @hash.transform_values(&:succ).should == { a: 2, b: 3, c: 4 } + end + + it "makes both hashes to share keys" do + key = [1, 2, 3] + new_hash = { key => 1 }.transform_values(&:succ) + new_hash[key].should == 2 + new_hash.keys[0].should equal(key) + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_values + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:succ).should == { a: 2, b: 3, c: 4 } + end + end + + it "returns a Hash instance, even on subclasses" do + klass = Class.new(Hash) + h = klass.new + h[:foo] = 42 + r = h.transform_values{|v| 2 * v} + 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 + before :each do + @hash = { a: 1, b: 2, c: 3 } + @initial_pairs = @hash.dup + end + + it "returns self" do + @hash.transform_values!(&:succ).should equal(@hash) + end + + it "updates self as transformed values with the given block" do + @hash.transform_values!(&:succ) + @hash.should == { a: 2, b: 3, c: 4 } + end + + it "partially modifies the contents if we broke from the block" do + @hash.transform_values! do |v| + break if v == 3 + 100 + v + end + @hash.should == { a: 101, b: 102, c: 3} + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_values! + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:succ) + @hash.should == { a: 2, b: 3, c: 4 } + end + end + + describe "on frozen instance" do + before :each do + @hash.freeze + end + + it "raises a FrozenError on an empty hash" do + ->{ {}.freeze.transform_values!(&:succ) }.should raise_error(FrozenError) + end + + it "keeps pairs and raises a FrozenError" do + ->{ @hash.transform_values!(&:succ) }.should raise_error(FrozenError) + @hash.should == @initial_pairs + end + + context "when no block is given" do + it "does not raise an exception" do + @hash.transform_values!.should be_an_instance_of(Enumerator) + end + end + end +end diff --git a/spec/ruby/core/hash/try_convert_spec.rb b/spec/ruby/core/hash/try_convert_spec.rb new file mode 100644 index 0000000000..d359ae49d8 --- /dev/null +++ b/spec/ruby/core/hash/try_convert_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.try_convert" do + it "returns the argument if it's a Hash" do + x = Hash.new + Hash.try_convert(x).should equal(x) + end + + it "returns the argument if it's a kind of Hash" do + x = HashSpecs::MyHash.new + Hash.try_convert(x).should equal(x) + end + + it "returns nil when the argument does not respond to #to_hash" do + Hash.try_convert(Object.new).should be_nil + end + + it "sends #to_hash to the argument and returns the result if it's nil" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(nil) + Hash.try_convert(obj).should be_nil + end + + it "sends #to_hash to the argument and returns the result if it's a Hash" do + x = Hash.new + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(x) + Hash.try_convert(obj).should equal(x) + end + + it "sends #to_hash to the argument and returns the result if it's a kind of Hash" do + x = HashSpecs::MyHash.new + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(x) + Hash.try_convert(obj).should equal(x) + end + + it "sends #to_hash to the argument and raises TypeError if it's not a kind of Hash" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(Object.new) + -> { Hash.try_convert obj }.should raise_error(TypeError, "can't convert MockObject to Hash (MockObject#to_hash gives Object)") + end + + it "does not rescue exceptions raised by #to_hash" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_raise(RuntimeError) + -> { Hash.try_convert obj }.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/hash/update_spec.rb b/spec/ruby/core/hash/update_spec.rb new file mode 100644 index 0000000000..0975045ad1 --- /dev/null +++ b/spec/ruby/core/hash/update_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/update' + +describe "Hash#update" do + it_behaves_like :hash_update, :update +end diff --git a/spec/ruby/core/hash/value_spec.rb b/spec/ruby/core/hash/value_spec.rb new file mode 100644 index 0000000000..0ab16a5d1b --- /dev/null +++ b/spec/ruby/core/hash/value_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/value' + +describe "Hash#value?" do + it_behaves_like :hash_value_p, :value? +end diff --git a/spec/ruby/core/hash/values_at_spec.rb b/spec/ruby/core/hash/values_at_spec.rb new file mode 100644 index 0000000000..b620a279ba --- /dev/null +++ b/spec/ruby/core/hash/values_at_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/values_at' + +describe "Hash#values_at" do + it_behaves_like :hash_values_at, :values_at +end diff --git a/spec/ruby/core/hash/values_spec.rb b/spec/ruby/core/hash/values_spec.rb new file mode 100644 index 0000000000..9f2a481a48 --- /dev/null +++ b/spec/ruby/core/hash/values_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#values" do + it "returns an array of values" do + h = { 1 => :a, 'a' => :a, 'the' => 'lang' } + h.values.should be_kind_of(Array) + h.values.sort {|a, b| a.to_s <=> b.to_s}.should == [:a, :a, 'lang'] + end +end |
