diff options
Diffstat (limited to 'spec/ruby/core/hash')
81 files changed, 3260 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..d607f235bd --- /dev/null +++ b/spec/ruby/core/hash/allocate_spec.rb @@ -0,0 +1,15 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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..ab8d320c18 --- /dev/null +++ b/spec/ruby/core/hash/any_spec.rb @@ -0,0 +1,30 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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.any?.should == false + + hash_with_members = { 'key' => 'value' } + hash_with_members.any?.should == true + 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..0b10720b6f --- /dev/null +++ b/spec/ruby/core/hash/assoc_spec.rb @@ -0,0 +1,50 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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 in Hash#[]= due to https://bugs.ruby-lang.org/issues/12855 + h = {}.compare_by_identity + k1 = 'pear' + h[k1] = :red + k2 = 'pear' + 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..ea8235451a --- /dev/null +++ b/spec/ruby/core/hash/clear_spec.rb @@ -0,0 +1,32 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.clear }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.clear }.should raise_error(RuntimeError) + 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..188ae1c807 --- /dev/null +++ b/spec/ruby/core/hash/clone_spec.rb @@ -0,0 +1,13 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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.object_id.should_not == hash.object_id + 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..d9ef8a2987 --- /dev/null +++ b/spec/ruby/core/hash/compact_spec.rb @@ -0,0 +1,61 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +ruby_version_is "2.4" do + 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 + 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 RuntimeError" do + ->{ @hash.compact! }.should raise_error(RuntimeError) + @hash.should == @initial_pairs + end + 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..a518e0cdb3 --- /dev/null +++ b/spec/ruby/core/hash/compare_by_identity_spec.rb @@ -0,0 +1,140 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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.compare_by_identity?.should == true + @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 RuntimeError on frozen hashes" do + @h = @h.freeze + lambda { @h.compare_by_identity }.should raise_error(RuntimeError) + end + + # Behaviour confirmed in bug #1871 + it "persists over #dups" do + @idh['foo'] = :bar + @idh['foo'] = :glark + @idh.dup.should == @idh + @idh.dup.size.should == @idh.size + end + + it "persists over #clones" do + @idh['foo'] = :bar + @idh['foo'] = :glark + @idh.clone.should == @idh + @idh.clone.size.should == @idh.size + end + + it "does not copy string keys" do + foo = 'foo' + @idh[foo] = true + @idh[foo] = true + @idh.size.should == 1 + @idh.keys.first.object_id.should == foo.object_id + end + + ruby_bug "#12855", "2.2.0"..."2.4.1" do + it "gives different identity for string literals" do + @idh['foo'] = 1 + @idh['foo'] = 2 + @idh.values.should == [1, 2] + @idh.size.should == 2 + end + 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..0f582d91de --- /dev/null +++ b/spec/ruby/core/hash/constructor_spec.rb @@ -0,0 +1,110 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 "ignores elements that are not arrays" do + -> { + Hash[[:a]].should == {} + }.should complain(/ignoring wrong elements/) + -> { + Hash[[:nil]].should == {} + }.should complain(/ignoring wrong elements/) + end + + it "raises an ArgumentError for arrays of more than 2 elements" do + lambda{ 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 + lambda { Hash[1, 2, 3] }.should raise_error(ArgumentError) + lambda { 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 "removes the default_proc" do + hash = Hash.new { |h, k| h[k] = [] } + Hash[hash].default_proc.should be_nil + 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..0bd20d43af --- /dev/null +++ b/spec/ruby/core/hash/default_proc_spec.rb @@ -0,0 +1,80 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda{{}.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 = {} + lambda do + h.default_proc = lambda {|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 = {} + lambda do + h.default_proc = lambda {|a| } + end.should raise_error(TypeError) + lambda do + h.default_proc = lambda {|a,b,c| } + end.should raise_error(TypeError) + end + + it "raises a RuntimeError if self is frozen" do + lambda { {}.freeze.default_proc = Proc.new {} }.should raise_error(RuntimeError) + lambda { {}.freeze.default_proc = nil }.should raise_error(RuntimeError) + 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..6c1c7377b7 --- /dev/null +++ b/spec/ruby/core/hash/default_spec.rb @@ -0,0 +1,46 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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, lambda { 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 RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.default = nil }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.default = nil }.should raise_error(RuntimeError) + 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..d739e4fbab --- /dev/null +++ b/spec/ruby/core/hash/delete_if_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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 RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.delete_if { false } }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.delete_if { true } }.should raise_error(RuntimeError) + 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..a45b8cd171 --- /dev/null +++ b/spec/ruby/core/hash/delete_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 { :defualt }.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 { :defualt }.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 "accepts keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + { key => 5 }.delete(key).should == 5 + end + + it "raises a RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.delete("foo") }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.delete("foo") }.should raise_error(RuntimeError) + 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..237c407e91 --- /dev/null +++ b/spec/ruby/core/hash/dig_spec.rb @@ -0,0 +1,68 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +ruby_version_is '2.3' do + 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 + lambda { { 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' ] ] + lambda { h.dig(:foo, 0, :bar, 0, 0) }.should raise_error(TypeError) + lambda { 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 +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..4a4078a594 --- /dev/null +++ b/spec/ruby/core/hash/each_key_spec.rb @@ -0,0 +1,23 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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..285ca01b26 --- /dev/null +++ b/spec/ruby/core/hash/each_pair_spec.rb @@ -0,0 +1,11 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../shared/each', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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..676fd284b9 --- /dev/null +++ b/spec/ruby/core/hash/each_spec.rb @@ -0,0 +1,11 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../shared/each', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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..d3b2b8692e --- /dev/null +++ b/spec/ruby/core/hash/each_value_spec.rb @@ -0,0 +1,23 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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..b5ca56e84b --- /dev/null +++ b/spec/ruby/core/hash/element_reference_spec.rb @@ -0,0 +1,120 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 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 +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..a2d67c7f22 --- /dev/null +++ b/spec/ruby/core/hash/element_set_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/store', __FILE__) + +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..84464f34e1 --- /dev/null +++ b/spec/ruby/core/hash/empty_spec.rb @@ -0,0 +1,15 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Hash#empty?" do + it "returns true if the hash has no entries" do + {}.empty?.should == true + { 1 => 1 }.empty?.should == false + end + + it "returns true if the hash has no entries and has a default value" do + Hash.new(5).empty?.should == true + Hash.new { 5 }.empty?.should == true + Hash.new { |hsh, k| hsh[k] = k }.empty?.should == true + 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..3e22bf1c67 --- /dev/null +++ b/spec/ruby/core/hash/eql_spec.rb @@ -0,0 +1,9 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/eql', __FILE__) + +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..76e4d796aa --- /dev/null +++ b/spec/ruby/core/hash/equal_value_spec.rb @@ -0,0 +1,18 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/eql', __FILE__) + +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/fetch_spec.rb b/spec/ruby/core/hash/fetch_spec.rb new file mode 100644 index 0000000000..5e701b1162 --- /dev/null +++ b/spec/ruby/core/hash/fetch_spec.rb @@ -0,0 +1,36 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Hash#fetch" do + it "returns the value for key" do + { a: 1, b: -1 }.fetch(:b).should == -1 + end + + it "raises a KeyError if key is not found" do + lambda { {}.fetch(:a) }.should raise_error(KeyError) + lambda { Hash.new(5).fetch(:a) }.should raise_error(KeyError) + lambda { Hash.new { 5 }.fetch(:a) }.should raise_error(KeyError) + 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 + lambda { + @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 + lambda { {}.fetch() }.should raise_error(ArgumentError) + lambda { {}.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..d6e47d5885 --- /dev/null +++ b/spec/ruby/core/hash/fetch_values_spec.rb @@ -0,0 +1,35 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +ruby_version_is "2.3" do + 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 + end + + describe "with unmatched keys" do + it "raises a KeyError" do + ->{ @hash.fetch_values :z }.should raise_error(KeyError) + ->{ @hash.fetch_values :a, :z }.should raise_error(KeyError) + end + + 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 +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..3d3df576cf --- /dev/null +++ b/spec/ruby/core/hash/fixtures/classes.rb @@ -0,0 +1,68 @@ +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 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/flatten_spec.rb b/spec/ruby/core/hash/flatten_spec.rb new file mode 100644 index 0000000000..60762f864d --- /dev/null +++ b/spec/ruby/core/hash/flatten_spec.rb @@ -0,0 +1,62 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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 + lambda 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..35bf52bf64 --- /dev/null +++ b/spec/ruby/core/hash/gt_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/comparison', __FILE__) +require File.expand_path('../shared/greater_than', __FILE__) + +ruby_version_is "2.3" do + 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 +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..5453440ed6 --- /dev/null +++ b/spec/ruby/core/hash/gte_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/comparison', __FILE__) +require File.expand_path('../shared/greater_than', __FILE__) + +ruby_version_is "2.3" do + 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 +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..1d2aa279f1 --- /dev/null +++ b/spec/ruby/core/hash/has_key_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/key', __FILE__) + +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..dc8fdf9b69 --- /dev/null +++ b/spec/ruby/core/hash/has_value_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/value', __FILE__) + +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..9d1e984c60 --- /dev/null +++ b/spec/ruby/core/hash/hash_spec.rb @@ -0,0 +1,36 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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 "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 +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..8731673c19 --- /dev/null +++ b/spec/ruby/core/hash/include_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/key', __FILE__) + +describe "Hash#include?" do + it_behaves_like(:hash_key_p, :include?) +end diff --git a/spec/ruby/core/hash/index_spec.rb b/spec/ruby/core/hash/index_spec.rb new file mode 100644 index 0000000000..28e64f4eb8 --- /dev/null +++ b/spec/ruby/core/hash/index_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/index', __FILE__) + +describe "Hash#index" do + it_behaves_like :hash_index, :index +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..aa943d333d --- /dev/null +++ b/spec/ruby/core/hash/initialize_spec.rb @@ -0,0 +1,40 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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.instance_eval { initialize { |_, k| k * 2 } } + h.default_proc.should_not == nil + h["a"].should == "aa" + 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 "returns self" do + h = Hash.new + h.send(:initialize).should equal(h) + end + + it "raises a RuntimeError if called on a frozen instance" do + block = lambda { HashSpecs.frozen_hash.instance_eval { initialize() }} + block.should raise_error(RuntimeError) + + block = lambda { HashSpecs.frozen_hash.instance_eval { initialize(nil) } } + block.should raise_error(RuntimeError) + + block = lambda { HashSpecs.frozen_hash.instance_eval { initialize(5) } } + block.should raise_error(RuntimeError) + + block = lambda { HashSpecs.frozen_hash.instance_eval { initialize { 5 } } } + block.should raise_error(RuntimeError) + 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..b8c42cf269 --- /dev/null +++ b/spec/ruby/core/hash/inspect_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/to_s', __FILE__) + +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..bc2bf711f7 --- /dev/null +++ b/spec/ruby/core/hash/invert_spec.rb @@ -0,0 +1,27 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 +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..6b3a1925d3 --- /dev/null +++ b/spec/ruby/core/hash/keep_if_spec.rb @@ -0,0 +1,37 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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 RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.keep_if { true } }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.keep_if { false } }.should raise_error(RuntimeError) + 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..dc78174641 --- /dev/null +++ b/spec/ruby/core/hash/key_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/key', __FILE__) +require File.expand_path('../shared/index', __FILE__) + +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..bc7855f73b --- /dev/null +++ b/spec/ruby/core/hash/keys_spec.rb @@ -0,0 +1,23 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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..90751402eb --- /dev/null +++ b/spec/ruby/core/hash/length_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/length', __FILE__) + +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..c32af32017 --- /dev/null +++ b/spec/ruby/core/hash/lt_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/comparison', __FILE__) +require File.expand_path('../shared/less_than', __FILE__) + +ruby_version_is "2.3" do + 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 +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..1a6926a44a --- /dev/null +++ b/spec/ruby/core/hash/lte_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../shared/comparison', __FILE__) +require File.expand_path('../shared/less_than', __FILE__) + +ruby_version_is "2.3" do + 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 +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..376bb4c006 --- /dev/null +++ b/spec/ruby/core/hash/member_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/key', __FILE__) + +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..21401ffd08 --- /dev/null +++ b/spec/ruby/core/hash/merge_spec.rb @@ -0,0 +1,77 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../shared/update', __FILE__) + +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 } + + lambda { + 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 + +end + +describe "Hash#merge!" do + it_behaves_like(:hash_update, :merge!) + + 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.merge!(hash2) } + hash.should == {1 => :foo, 3 => :bar, 5 => 6} + end +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..c31cc2abf4 --- /dev/null +++ b/spec/ruby/core/hash/new_spec.rb @@ -0,0 +1,36 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + lambda { Hash.new(5,6) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed both default argument and default block" do + lambda { Hash.new(5) { 0 } }.should raise_error(ArgumentError) + lambda { Hash.new(nil) { 0 } }.should raise_error(ArgumentError) + 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..79c633f95e --- /dev/null +++ b/spec/ruby/core/hash/rassoc_spec.rb @@ -0,0 +1,42 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +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..09315737b6 --- /dev/null +++ b/spec/ruby/core/hash/rehash_spec.rb @@ -0,0 +1,42 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +describe "Hash#rehash" do + it "reorganizes the hash by recomputing all key hash codes" do + k1 = [1] + k2 = [2] + h = {} + h[k1] = 0 + h[k2] = 1 + + k1 << 2 + h.key?(k1).should == false + h.keys.include?(k1).should == true + + h.rehash.should equal(h) + h.key?(k1).should == true + h[k1].should == 0 + + 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 "raises a RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.rehash }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.rehash }.should raise_error(RuntimeError) + 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..21dd7425aa --- /dev/null +++ b/spec/ruby/core/hash/reject_spec.rb @@ -0,0 +1,100 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +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 + + it "does not taint the resulting hash" do + h = { a: 1 }.taint + h.reject {false}.tainted?.should == false + 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_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 RuntimeError if called on a frozen instance that is modified" do + lambda { HashSpecs.empty_frozen_hash.reject! { true } }.should raise_error(RuntimeError) + end + + it "raises a RuntimeError if called on a frozen instance that would not be modified" do + lambda { HashSpecs.frozen_hash.reject! { false } }.should raise_error(RuntimeError) + 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..61b3164355 --- /dev/null +++ b/spec/ruby/core/hash/replace_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/replace', __FILE__) + +describe "Hash#replace" do + it_behaves_like(:hash_replace, :replace) +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..449607b606 --- /dev/null +++ b/spec/ruby/core/hash/select_spec.rb @@ -0,0 +1,83 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/iteration', __FILE__) +require File.expand_path('../../enumerable/shared/enumeratorized', __FILE__) + +describe "Hash#select" 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 }.select { |*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 }.select { |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.select { |*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.select.should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator when called on an empty hash without a block" do + @empty.select.should be_an_instance_of(Enumerator) + end + + it_behaves_like(:hash_iteration_no_block, :select) + it_behaves_like(:enumeratorized_with_origin_size, :select, { 1 => 2, 3 => 4, 5 => 6 }) +end + +describe "Hash#select!" 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.select! { |k,v| v <= 1 }.should equal h + + h = { 1 => 2, 3 => 4 } + all_args_select = [] + h.dup.select! { |*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.select! { |k,v| false }.should equal(h) + h.should == {} + end + + it "returns nil if no changes were made" do + { a: 1 }.select! { |k,v| v <= 1 }.should == nil + end + + it "raises a RuntimeError if called on an empty frozen instance" do + lambda { HashSpecs.empty_frozen_hash.select! { false } }.should raise_error(RuntimeError) + end + + it "raises a RuntimeError if called on a frozen instance that would not be modified" do + lambda { HashSpecs.frozen_hash.select! { true } }.should raise_error(RuntimeError) + end + + it_behaves_like(:hash_iteration_no_block, :select!) + it_behaves_like(:enumeratorized_with_origin_size, :select!, { 1 => 2, 3 => 4, 5 => 6 }) +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..bbb9bfd6ad --- /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 + lambda { { a: 1 }.send(@method, 1) }.should raise_error(TypeError) + lambda { { a: 1 }.send(@method, nil) }.should raise_error(TypeError) + lambda { { 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..bf4c569cfc --- /dev/null +++ b/spec/ruby/core/hash/shared/each.rb @@ -0,0 +1,68 @@ +describe :hash_each, shared: true do + 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 "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..1aed5f51fb --- /dev/null +++ b/spec/ruby/core/hash/shared/eql.rb @@ -0,0 +1,216 @@ +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 iff 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) + + # It's undefined whether the impl does a[0].eql?(a[1]) or + # a[1].eql?(a[0]) so we taint both. + def obj.eql?(o) + return true if self.equal?(o) + taint + o.taint + false + end + + obj + end + + { a[0] => 1 }.send(@method, { a[1] => 1 }).should be_false + a[0].tainted?.should be_true + a[1].tainted?.should be_true + + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + # It's undefined whether the impl does a[0].send(@method, a[1]) or + # a[1].send(@method, a[0]) so we taint both. + taint + o.taint + true + end + + obj + end + + { a[0] => 1 }.send(@method, { a[1] => 1 }).should be_true + a[0].tainted?.should be_true + a[1].tainted?.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/equal.rb b/spec/ruby/core/hash/shared/equal.rb new file mode 100644 index 0000000000..43606437fe --- /dev/null +++ b/spec/ruby/core/hash/shared/equal.rb @@ -0,0 +1,90 @@ +describe :hash_equal, 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).and_return(0) + y = mock('y') + y.should_receive(:hash).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).and_return(0) + y.should_receive(:hash).and_return(1) + + def x.hash() 0 end + def y.hash() 1 end + + { 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 "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 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..9aa1b2a46f --- /dev/null +++ b/spec/ruby/core/hash/shared/index.rb @@ -0,0 +1,27 @@ +require File.expand_path('../../../../spec_helper', __FILE__) +require File.expand_path('../../fixtures/classes', __FILE__) + +describe :hash_index, shared: true do + it "returns the corresponding key for value" do + { 2 => 'a', 1 => 'b' }.send(@method, 'b').should == 1 + end + + it "returns nil if the value is not found" do + { a: -1, b: 3.14, c: 2.718 }.send(@method, 1).should be_nil + end + + it "doesn't return default value if the value is not found" do + Hash.new(5).send(@method, 5).should be_nil + end + + it "compares values using ==" do + { 1 => 0 }.send(@method, 0.0).should == 1 + { 1 => 0.0 }.send(@method, 0).should == 1 + + needle = mock('needle') + inhash = mock('inhash') + inhash.should_receive(:==).with(needle).and_return(true) + + { 1 => inhash }.send(@method, needle).should == 1 + 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/replace.rb b/spec/ruby/core/hash/shared/replace.rb new file mode 100644 index 0000000000..463c861395 --- /dev/null +++ b/spec/ruby/core/hash/shared/replace.rb @@ -0,0 +1,51 @@ +describe :hash_replace, shared: true do + it "replaces the contents of self with other" do + h = { a: 1, b: 2 } + h.send(@method, 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.send(@method, obj) + h.should == { 1 => 2, 3 => 4 } + end + + it "calls to_hash on hash subclasses" do + h = {} + h.send(@method, HashSpecs::ToHashHash[1 => 2]) + h.should == { 1 => 2 } + end + + it "does not transfer default values" do + hash_a = {} + hash_b = Hash.new(5) + hash_a.send(@method, hash_b) + hash_a.default.should == 5 + + hash_a = {} + hash_b = Hash.new { |h, k| k * 2 } + hash_a.send(@method, hash_b) + hash_a.default(5).should == 10 + + hash_a = Hash.new { |h, k| k * 5 } + hash_b = Hash.new(lambda { raise "Should not invoke lambda" }) + hash_a.send(@method, hash_b) + hash_a.default.should == hash_b.default + end + + it "raises a RuntimeError if called on a frozen instance that would not be modified" do + lambda do + HashSpecs.frozen_hash.send(@method, HashSpecs.frozen_hash) + end.should raise_error(RuntimeError) + end + + it "raises a RuntimeError if called on a frozen instance that is modified" do + lambda do + HashSpecs.frozen_hash.send(@method, HashSpecs.empty_frozen_hash) + end.should raise_error(RuntimeError) + end +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..b43dcbc93e --- /dev/null +++ b/spec/ruby/core/hash/shared/store.rb @@ -0,0 +1,98 @@ +require File.expand_path('../../fixtures/classes', __FILE__) + +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 a Bignum 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].frozen?.should == true + 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" + key2 = "foo" + 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 RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.send(@method, 1, 2) }.should raise_error(RuntimeError) + 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 +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..0afe605826 --- /dev/null +++ b/spec/ruby/core/hash/shared/to_s.rb @@ -0,0 +1,109 @@ +require File.expand_path('../../../../spec_helper', __FILE__) +require File.expand_path('../../fixtures/classes', __FILE__) + +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 } + + pairs = [] + h.each do |key, value| + pairs << key.inspect + '=>' + value.inspect + end + + str = '{' + pairs.join(', ') + '}' + h.send(@method).should == str + 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') + + { key => val }.send(@method).should == '{key=>val}' + end + + it "does not call #to_s on a String returned from #inspect" do + str = "abc" + str.should_not_receive(:to_s) + + { a: str }.send(@method).should == '{:a=>"abc"}' + 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") + + { a: obj }.send(@method).should == "{:a=>abc}" + 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) + + { a: obj }.send(@method).should =~ /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/ + 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) + + { a: obj }.send(@method).should =~ /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/ + 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) + + lambda { { a: obj }.send(@method) }.should raise_error(Exception) + end + + it "handles hashes with recursive values" do + x = {} + x[0] = x + x.send(@method).should == '{0=>{...}}' + + x = {} + y = {} + x[0] = y + y[1] = x + x.send(@method).should == "{0=>{1=>{...}}}" + y.send(@method).should == "{1=>{0=>{...}}}" + end + + it "returns a tainted string if self is tainted and not empty" do + {}.taint.send(@method).tainted?.should be_false + { nil => nil }.taint.send(@method).tainted?.should be_true + end + + it "returns an untrusted string if self is untrusted and not empty" do + {}.untrust.send(@method).untrusted?.should be_false + { nil => nil }.untrust.send(@method).untrusted?.should be_true + end + + ruby_version_is ''...'2.3' do + it "raises 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)) + + lambda { + {a: utf_16be}.send(@method) + }.should raise_error(Encoding::CompatibilityError) + end + end + + ruby_version_is '2.3' do + 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)) + + {a: utf_16be}.send(@method).should == '{:a=>"utf_16be \u3042"}' + 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..b1e3793028 --- /dev/null +++ b/spec/ruby/core/hash/shared/update.rb @@ -0,0 +1,59 @@ +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 RuntimeError on a frozen instance that is modified" do + lambda do + HashSpecs.frozen_hash.send(@method, 1 => 2) + end.should raise_error(RuntimeError) + 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 + + lambda { HashSpecs.frozen_hash.send(@method, obj) }.should raise_error(RuntimeError) + end + + # see redmine #1571 + it "raises a RuntimeError on a frozen instance that would not be modified" do + lambda do + HashSpecs.frozen_hash.send(@method, HashSpecs.empty_frozen_hash) + end.should raise_error(RuntimeError) + 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..3991da9656 --- /dev/null +++ b/spec/ruby/core/hash/shift_spec.rb @@ -0,0 +1,64 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 "calls #default with nil if the Hash is empty" do + h = {} + def h.default(key) + key.should == nil + :foo + end + h.shift.should == :foo + end + + it "returns nil from an empty hash" do + {}.shift.should == nil + end + + it "returns (computed) default for empty hashes" do + Hash.new(5).shift.should == 5 + h = Hash.new { |*args| args } + h.shift.should == [h, 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 RuntimeError if called on a frozen instance" do + lambda { HashSpecs.frozen_hash.shift }.should raise_error(RuntimeError) + lambda { HashSpecs.empty_frozen_hash.shift }.should raise_error(RuntimeError) + 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..71660af038 --- /dev/null +++ b/spec/ruby/core/hash/size_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/length', __FILE__) + +describe "Hash#size" do + it_behaves_like(:hash_length, :size) +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..06235f7609 --- /dev/null +++ b/spec/ruby/core/hash/sort_spec.rb @@ -0,0 +1,17 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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..45ea8da896 --- /dev/null +++ b/spec/ruby/core/hash/store_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/store', __FILE__) + +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..76ca721038 --- /dev/null +++ b/spec/ruby/core/hash/to_a_spec.rb @@ -0,0 +1,37 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 + + it "returns a tainted array if self is tainted" do + {}.taint.to_a.tainted?.should be_true + end + + it "returns an untrusted array if self is untrusted" do + {}.untrust.to_a.untrusted?.should be_true + 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..366fd58e8a --- /dev/null +++ b/spec/ruby/core/hash/to_h_spec.rb @@ -0,0 +1,34 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 "copies the default" do + @h.default = 42 + @h.to_h.default.should == 42 + @h[:hello].should == 42 + end + + it "copies 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 + 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..5a173c2ab5 --- /dev/null +++ b/spec/ruby/core/hash/to_hash_spec.rb @@ -0,0 +1,14 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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..b486d188b7 --- /dev/null +++ b/spec/ruby/core/hash/to_proc_spec.rb @@ -0,0 +1,89 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +ruby_version_is "2.3" do + 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 not a lambda" do + @proc.lambda?.should == false + end + + it "raises ArgumentError if not passed exactly one argument" do + lambda { + @proc.call + }.should raise_error(ArgumentError) + + lambda { + @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 + lambda { @hash.to_proc.call }.should raise_error(ArgumentError) + end + 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..803b1d6236 --- /dev/null +++ b/spec/ruby/core/hash/to_s_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/to_s', __FILE__) + +describe "Hash#to_s" do + it_behaves_like :hash_to_s, :to_s +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..7e4ff45bea --- /dev/null +++ b/spec/ruby/core/hash/transform_values_spec.rb @@ -0,0 +1,71 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +ruby_version_is "2.4" do + 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 + + 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 + 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 + + 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 "keeps pairs and raises a RuntimeError" do + ->{ @hash.transform_values!(&:succ) }.should raise_error(RuntimeError) + @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 +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..818b08fb32 --- /dev/null +++ b/spec/ruby/core/hash/try_convert_spec.rb @@ -0,0 +1,50 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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) + lambda { Hash.try_convert obj }.should raise_error(TypeError) + end + + it "does not rescue exceptions raised by #to_hash" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_raise(RuntimeError) + lambda { 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..6cfedea271 --- /dev/null +++ b/spec/ruby/core/hash/update_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/update', __FILE__) + +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..acfe1968d5 --- /dev/null +++ b/spec/ruby/core/hash/value_spec.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/value', __FILE__) + +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..7c39e9b573 --- /dev/null +++ b/spec/ruby/core/hash/values_at_spec.rb @@ -0,0 +1,7 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) +require File.expand_path('../shared/values_at', __FILE__) + +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..4ce2f3f5f0 --- /dev/null +++ b/spec/ruby/core/hash/values_spec.rb @@ -0,0 +1,10 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/classes', __FILE__) + +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 |