Skip to content

Commit 5b3d092

Browse files
committed
Move Hash specs to rely less on shared examples
This is mostly a mechanical translation. A few things of interest: Two shared specs weren't actually shared. `Hash#index` used to be an alias for `Hash#key` but was removed at some point. `Hash#values_at` had shared specs which never were actually shared. Tests for `Hash#[]=` used `send`. I explicitly added back a test usign `send` to assert the return value. I cleaned up requires, each test file can be run in isolation. For `../mspec/bin/mspec core/hash`: ``` Before: 69 files, 633 examples, 1176 expectations, 0 failures, 0 errors, 0 tagged After: 69 files, 558 examples, 1026 expectations, 0 failures, 0 errors, 0 tagged ``` So 75 fewer specs, 150 fewer assertions
1 parent 38c283b commit 5b3d092

30 files changed

Lines changed: 649 additions & 715 deletions

CONTRIBUTING.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ to avoid duplication of specs, we have shared specs that are re-used in other sp
229229
bit tricky however, so let's go over it.
230230

231231
Commonly, if a shared spec is only reused within its own module, the shared spec will live within a
232-
shared directory inside that module's directory. For example, the `core/hash/shared/key.rb` spec is
232+
shared directory inside that module's directory. For example, the `core/hash/shared/iteration.rb` spec is
233233
only used by `Hash` specs, and so it lives inside `core/hash/shared/`.
234234

235235
When a shared spec is used across multiple modules or classes, it lives within the `shared/` directory.
@@ -243,25 +243,25 @@ variables from the implementor spec: `@method` and `@object`, which the implemen
243243
Here's an example of a snippet of a shared spec and two specs which integrates it:
244244

245245
```ruby
246-
# core/hash/shared/key.rb
247-
describe :hash_key_p, shared: true do
248-
it "returns true if the key's matching value was false" do
249-
{ xyz: false }.send(@method, :xyz).should == true
246+
# core/hash/shared/iteration.rb
247+
describe :hash_iteration_no_block, shared: true do
248+
it "returns an Enumerator if called on a non-empty hash without a block" do
249+
{}.send(@method).should.instance_of?(Enumerator)
250250
end
251251
end
252252

253-
# core/hash/key_spec.rb
254-
describe "Hash#key?" do
255-
it_behaves_like :hash_key_p, :key?
253+
# core/hash/select_spec.rb
254+
describe "Hash#select" do
255+
it_behaves_like :hash_iteration_no_block, :select
256256
end
257257

258-
# core/hash/include_spec.rb
259-
describe "Hash#include?" do
260-
it_behaves_like :hash_key_p, :include?
258+
# core/hash/reject_spec.rb
259+
describe "Hash#reject?" do
260+
it_behaves_like :hash_iteration_no_block, :reject
261261
end
262262
```
263263

264-
In the example, the first `describe` defines the shared spec `:hash_key_p`, which defines a spec that
264+
In the example, the first `describe` defines the shared spec `:hash_iteration_no_block`, which defines a spec that
265265
calls the `@method` method with an expectation. In the implementor spec, we use `it_behaves_like` to
266266
integrate the shared spec. `it_behaves_like` takes 3 parameters: the key of the shared spec, a method,
267267
and an object. These last two parameters are accessible via `@method` and `@object` in the shared spec.

core/hash/each_pair_spec.rb

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,111 @@
11
require_relative '../../spec_helper'
2-
require_relative 'fixtures/classes'
32
require_relative 'shared/iteration'
4-
require_relative 'shared/each'
53
require_relative '../enumerable/shared/enumeratorized'
64

75
describe "Hash#each_pair" do
8-
it_behaves_like :hash_each, :each_pair
96
it_behaves_like :hash_iteration_no_block, :each_pair
107
it_behaves_like :enumeratorized_with_origin_size, :each_pair, { 1 => 2, 3 => 4, 5 => 6 }
8+
9+
# This is inconsistent with below, MRI checks the block arity in rb_hash_each_pair()
10+
it "yields a [[key, value]] Array for each pair to a block expecting |*args|" do
11+
all_args = []
12+
{ 1 => 2, 3 => 4 }.each_pair { |*args| all_args << args }
13+
all_args.sort.should == [[[1, 2]], [[3, 4]]]
14+
end
15+
16+
it "yields the key and value of each pair to a block expecting |key, value|" do
17+
r = {}
18+
h = { a: 1, b: 2, c: 3, d: 5 }
19+
h.each_pair { |k,v| r[k.to_s] = v.to_s }.should.equal?(h)
20+
r.should == { "a" => "1", "b" => "2", "c" => "3", "d" => "5" }
21+
end
22+
23+
it "yields the key only to a block expecting |key,|" do
24+
ary = []
25+
h = { "a" => 1, "b" => 2, "c" => 3 }
26+
h.each_pair { |k,| ary << k }
27+
ary.sort.should == ["a", "b", "c"]
28+
end
29+
30+
it "always yields an Array of 2 elements, even when given a callable of arity 2" do
31+
obj = Object.new
32+
def obj.foo(key, value)
33+
end
34+
35+
-> {
36+
{ "a" => 1 }.each_pair(&obj.method(:foo))
37+
}.should.raise(ArgumentError)
38+
39+
-> {
40+
{ "a" => 1 }.each_pair(&-> key, value { })
41+
}.should.raise(ArgumentError)
42+
end
43+
44+
it "yields an Array of 2 elements when given a callable of arity 1" do
45+
obj = Object.new
46+
def obj.foo(key_value)
47+
ScratchPad << key_value
48+
end
49+
50+
ScratchPad.record([])
51+
{ "a" => 1 }.each_pair(&obj.method(:foo))
52+
ScratchPad.recorded.should == [["a", 1]]
53+
end
54+
55+
it "raises an error for a Hash when an arity enforcing callable of arity >2 is passed in" do
56+
obj = Object.new
57+
def obj.foo(key, value, extra)
58+
end
59+
60+
-> {
61+
{ "a" => 1 }.each_pair(&obj.method(:foo))
62+
}.should.raise(ArgumentError)
63+
end
64+
65+
it "uses the same order as keys() and values()" do
66+
h = { a: 1, b: 2, c: 3, d: 5 }
67+
keys = []
68+
values = []
69+
70+
h.each_pair do |k, v|
71+
keys << k
72+
values << v
73+
end
74+
75+
keys.should == h.keys
76+
values.should == h.values
77+
end
78+
79+
# Confirming the argument-splatting works from child class for both k, v and [k, v]
80+
it "properly expands (or not) child class's 'each'-yielded args" do
81+
cls1 = Class.new(Hash) do
82+
attr_accessor :k_v
83+
def each
84+
super do |k, v|
85+
@k_v = [k, v]
86+
yield k, v
87+
end
88+
end
89+
end
90+
91+
cls2 = Class.new(Hash) do
92+
attr_accessor :k_v
93+
def each
94+
super do |k, v|
95+
@k_v = [k, v]
96+
yield([k, v])
97+
end
98+
end
99+
end
100+
101+
obj1 = cls1.new
102+
obj1['a'] = 'b'
103+
obj1.map {|k, v| [k, v]}.should == [['a', 'b']]
104+
obj1.k_v.should == ['a', 'b']
105+
106+
obj2 = cls2.new
107+
obj2['a'] = 'b'
108+
obj2.map {|k, v| [k, v]}.should == [['a', 'b']]
109+
obj2.k_v.should == ['a', 'b']
110+
end
11111
end

core/hash/each_spec.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
require_relative '../../spec_helper'
2-
require_relative 'fixtures/classes'
3-
require_relative 'shared/iteration'
4-
require_relative 'shared/each'
5-
require_relative '../enumerable/shared/enumeratorized'
62

73
describe "Hash#each" do
8-
it_behaves_like :hash_each, :each
9-
it_behaves_like :hash_iteration_no_block, :each
10-
it_behaves_like :enumeratorized_with_origin_size, :each, { 1 => 2, 3 => 4, 5 => 6 }
4+
it "is an alias of Hash#each_pair" do
5+
Hash.instance_method(:each).should == Hash.instance_method(:each_pair)
6+
end
117
end

core/hash/element_set_spec.rb

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,121 @@
11
require_relative '../../spec_helper'
22
require_relative 'fixtures/classes'
3-
require_relative 'shared/store'
43

54
describe "Hash#[]=" do
6-
it_behaves_like :hash_store, :[]=
5+
it "associates the key with the value and return the value" do
6+
h = { a: 1 }
7+
h[:b] = 2
8+
h.should == { b:2, a:1 }
9+
end
10+
11+
it "returns the assigned value" do
12+
h = {}
13+
h.send(:[]=, 1, 2).should == 2
14+
end
15+
16+
it "duplicates string keys using dup semantics" do
17+
# dup doesn't copy singleton methods
18+
key = +"foo"
19+
def key.reverse() "bar" end
20+
h = {}
21+
h[key] = 0
22+
h.keys[0].reverse.should == "oof"
23+
end
24+
25+
it "stores unequal keys that hash to the same value" do
26+
h = {}
27+
k1 = ["x"]
28+
k2 = ["y"]
29+
# So they end up in the same bucket
30+
k1.should_receive(:hash).and_return(0)
31+
k2.should_receive(:hash).and_return(0)
32+
33+
h[k1] = 1
34+
h[k2] = 2
35+
h.size.should == 2
36+
end
37+
38+
it "accepts keys with private #hash method" do
39+
key = HashSpecs::KeyWithPrivateHash.new
40+
h = {}
41+
h[key] = "foo"
42+
h[key].should == "foo"
43+
end
44+
45+
it " accepts keys with an Integer hash" do
46+
o = mock(hash: 1 << 100)
47+
h = {}
48+
h[o] = 1
49+
h[o].should == 1
50+
end
51+
52+
it "duplicates and freezes string keys" do
53+
key = +"foo"
54+
h = {}
55+
h[key] = 0
56+
key << "bar"
57+
58+
h.should == { "foo" => 0 }
59+
h.keys[0].should.frozen?
60+
end
61+
62+
it "doesn't duplicate and freeze already frozen string keys" do
63+
key = "foo".freeze
64+
h = {}
65+
h[key] = 0
66+
h.keys[0].should.equal?(key)
67+
end
68+
69+
it "keeps the existing key in the hash if there is a matching one" do
70+
h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 }
71+
key1 = HashSpecs::ByValueKey.new(13)
72+
key2 = HashSpecs::ByValueKey.new(13)
73+
h[key1] = 41
74+
key_in_hash = h.keys.last
75+
key_in_hash.should.equal?(key1)
76+
h[key2] = 42
77+
last_key = h.keys.last
78+
last_key.should.equal?(key_in_hash)
79+
last_key.should_not.equal?(key2)
80+
end
81+
82+
it "keeps the existing String key in the hash if there is a matching one" do
83+
h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 }
84+
key1 = "foo".dup
85+
key2 = "foo".dup
86+
key1.should_not.equal?(key2)
87+
h[key1] = 41
88+
frozen_key = h.keys.last
89+
frozen_key.should_not.equal?(key1)
90+
h[key2] = 42
91+
h.keys.last.should.equal?(frozen_key)
92+
h.keys.last.should_not.equal?(key2)
93+
end
94+
95+
it "raises a FrozenError if called on a frozen instance" do
96+
-> { HashSpecs.frozen_hash[1] = 2 }.should.raise(FrozenError)
97+
end
98+
99+
it "does not raise an exception if changing the value of an existing key during iteration" do
100+
hash = {1 => 2, 3 => 4, 5 => 6}
101+
hash.each { hash[1] = :foo }
102+
hash.should == {1 => :foo, 3 => 4, 5 => 6}
103+
end
104+
105+
it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do
106+
code = <<-EOC
107+
load '#{fixture __FILE__, "name.rb"}'
108+
hash = {}
109+
[true, false, 1, 2.0, "hello", :ok].each do |value|
110+
hash[value] = 42
111+
raise "incorrect value" unless hash[value] == 42
112+
hash[value] = 43
113+
raise "incorrect value" unless hash[value] == 43
114+
end
115+
puts "OK"
116+
puts hash.size
117+
EOC
118+
result = ruby_exe(code, args: "2>&1")
119+
result.should == "OK\n6\n"
120+
end
7121
end

core/hash/filter_spec.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
require_relative '../../spec_helper'
2-
require_relative 'shared/select'
32

43
describe "Hash#filter" do
5-
it_behaves_like :hash_select, :filter
4+
it "is an alias of Hash#select" do
5+
Hash.instance_method(:filter).should == Hash.instance_method(:select)
6+
end
67
end
78

89
describe "Hash#filter!" do
9-
it_behaves_like :hash_select!, :filter!
10+
it "is an alias of Hash#select!" do
11+
Hash.instance_method(:filter!).should == Hash.instance_method(:select!)
12+
end
1013
end

core/hash/has_key_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require_relative '../../spec_helper'
2-
require_relative 'fixtures/classes'
3-
require_relative 'shared/key'
42

53
describe "Hash#has_key?" do
6-
it_behaves_like :hash_key_p, :has_key?
4+
it "is an alias of Hash#include?" do
5+
Hash.instance_method(:has_key?).should == Hash.instance_method(:include?)
6+
end
77
end

core/hash/has_value_spec.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
require_relative '../../spec_helper'
2-
require_relative 'fixtures/classes'
3-
require_relative 'shared/value'
42

53
describe "Hash#has_value?" do
6-
it_behaves_like :hash_value_p, :has_value?
4+
it "returns true if the value exists in the hash" do
5+
{ a: :b }.has_value?(:a).should == false
6+
{ 1 => 2 }.has_value?(2).should == true
7+
h = Hash.new(5)
8+
h.has_value?(5).should == false
9+
h = Hash.new { 5 }
10+
h.has_value?(5).should == false
11+
end
12+
13+
it "uses == semantics for comparing values" do
14+
{ 5 => 2.0 }.has_value?(2).should == true
15+
end
716
end

core/hash/include_spec.rb

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
require_relative '../../spec_helper'
2-
require_relative 'fixtures/classes'
3-
require_relative 'shared/key'
42

53
describe "Hash#include?" do
6-
it_behaves_like :hash_key_p, :include?
4+
it "returns true if argument is a key" do
5+
h = { a: 1, b: 2, c: 3, 4 => 0 }
6+
h.include?(:a).should == true
7+
h.include?(:b).should == true
8+
h.include?(2).should == false
9+
h.include?(4).should == true
10+
11+
not_supported_on :opal do
12+
h.include?('b').should == false
13+
h.include?(4.0).should == false
14+
end
15+
end
16+
17+
it "returns true if the key's matching value was nil" do
18+
{ xyz: nil }.include?(:xyz).should == true
19+
end
20+
21+
it "returns true if the key's matching value was false" do
22+
{ xyz: false }.include?(:xyz).should == true
23+
end
24+
25+
it "returns true if the key is nil" do
26+
{ nil => 'b' }.include?(nil).should == true
27+
{ nil => nil }.include?(nil).should == true
28+
end
29+
30+
it "compares keys with the same #hash value via #eql?" do
31+
x = mock('x')
32+
x.stub!(:hash).and_return(42)
33+
34+
y = mock('y')
35+
y.stub!(:hash).and_return(42)
36+
y.should_receive(:eql?).and_return(false)
37+
38+
{ x => nil }.include?(y).should == false
39+
end
740
end

0 commit comments

Comments
 (0)