rspec-expectations 3.0.0.beta1 → 3.0.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. data.tar.gz.sig +2 -2
  2. data/.yardopts +1 -0
  3. data/Changelog.md +138 -0
  4. data/README.md +75 -8
  5. data/features/README.md +2 -2
  6. data/features/built_in_matchers/README.md +12 -9
  7. data/features/built_in_matchers/comparisons.feature +2 -2
  8. data/features/built_in_matchers/contain_exactly.feature +46 -0
  9. data/features/built_in_matchers/expect_change.feature +2 -2
  10. data/features/built_in_matchers/include.feature +0 -48
  11. data/features/built_in_matchers/output.feature +70 -0
  12. data/features/composing_matchers.feature +250 -0
  13. data/features/compound_expectations.feature +45 -0
  14. data/features/custom_matchers/access_running_example.feature +1 -1
  15. data/features/custom_matchers/define_matcher.feature +6 -6
  16. data/features/custom_matchers/define_matcher_outside_rspec.feature +4 -8
  17. data/features/test_frameworks/{test_unit.feature → minitest.feature} +11 -11
  18. data/lib/rspec/expectations.rb +31 -42
  19. data/lib/rspec/expectations/diff_presenter.rb +141 -0
  20. data/lib/rspec/expectations/differ.rb +22 -132
  21. data/lib/rspec/expectations/encoded_string.rb +56 -0
  22. data/lib/rspec/expectations/expectation_target.rb +0 -30
  23. data/lib/rspec/expectations/fail_with.rb +2 -2
  24. data/lib/rspec/expectations/handler.rb +128 -31
  25. data/lib/rspec/expectations/minitest_integration.rb +16 -0
  26. data/lib/rspec/expectations/syntax.rb +4 -58
  27. data/lib/rspec/expectations/version.rb +1 -1
  28. data/lib/rspec/matchers.rb +298 -60
  29. data/lib/rspec/matchers/aliased_matcher.rb +35 -0
  30. data/lib/rspec/matchers/built_in.rb +37 -33
  31. data/lib/rspec/matchers/built_in/base_matcher.rb +25 -15
  32. data/lib/rspec/matchers/built_in/be.rb +23 -31
  33. data/lib/rspec/matchers/built_in/be_between.rb +55 -0
  34. data/lib/rspec/matchers/built_in/be_within.rb +15 -11
  35. data/lib/rspec/matchers/built_in/change.rb +198 -81
  36. data/lib/rspec/matchers/built_in/compound.rb +106 -0
  37. data/lib/rspec/matchers/built_in/contain_exactly.rb +245 -0
  38. data/lib/rspec/matchers/built_in/eq.rb +43 -4
  39. data/lib/rspec/matchers/built_in/eql.rb +2 -2
  40. data/lib/rspec/matchers/built_in/equal.rb +35 -18
  41. data/lib/rspec/matchers/built_in/has.rb +16 -15
  42. data/lib/rspec/matchers/built_in/include.rb +45 -23
  43. data/lib/rspec/matchers/built_in/match.rb +6 -3
  44. data/lib/rspec/matchers/built_in/operators.rb +103 -0
  45. data/lib/rspec/matchers/built_in/output.rb +108 -0
  46. data/lib/rspec/matchers/built_in/raise_error.rb +9 -15
  47. data/lib/rspec/matchers/built_in/respond_to.rb +5 -4
  48. data/lib/rspec/matchers/built_in/satisfy.rb +4 -3
  49. data/lib/rspec/matchers/built_in/start_and_end_with.rb +37 -16
  50. data/lib/rspec/matchers/built_in/throw_symbol.rb +6 -5
  51. data/lib/rspec/matchers/built_in/yield.rb +31 -29
  52. data/lib/rspec/matchers/composable.rb +138 -0
  53. data/lib/rspec/matchers/dsl.rb +330 -0
  54. data/lib/rspec/matchers/generated_descriptions.rb +6 -6
  55. data/lib/rspec/matchers/matcher_delegator.rb +33 -0
  56. data/lib/rspec/matchers/pretty.rb +13 -2
  57. data/spec/rspec/expectations/{differ_spec.rb → diff_presenter_spec.rb} +56 -36
  58. data/spec/rspec/expectations/encoded_string_spec.rb +74 -0
  59. data/spec/rspec/expectations/extensions/kernel_spec.rb +11 -11
  60. data/spec/rspec/expectations/fail_with_spec.rb +8 -8
  61. data/spec/rspec/expectations/handler_spec.rb +27 -49
  62. data/spec/rspec/expectations/minitest_integration_spec.rb +27 -0
  63. data/spec/rspec/expectations/syntax_spec.rb +17 -67
  64. data/spec/rspec/expectations_spec.rb +7 -52
  65. data/spec/rspec/matchers/aliased_matcher_spec.rb +48 -0
  66. data/spec/rspec/matchers/aliases_spec.rb +449 -0
  67. data/spec/rspec/matchers/{base_matcher_spec.rb → built_in/base_matcher_spec.rb} +24 -3
  68. data/spec/rspec/matchers/built_in/be_between_spec.rb +159 -0
  69. data/spec/rspec/matchers/{be_instance_of_spec.rb → built_in/be_instance_of_spec.rb} +0 -0
  70. data/spec/rspec/matchers/{be_kind_of_spec.rb → built_in/be_kind_of_spec.rb} +0 -0
  71. data/spec/rspec/matchers/{be_spec.rb → built_in/be_spec.rb} +76 -32
  72. data/spec/rspec/matchers/{be_within_spec.rb → built_in/be_within_spec.rb} +6 -2
  73. data/spec/rspec/matchers/{change_spec.rb → built_in/change_spec.rb} +310 -69
  74. data/spec/rspec/matchers/built_in/compound_spec.rb +292 -0
  75. data/spec/rspec/matchers/built_in/contain_exactly_spec.rb +441 -0
  76. data/spec/rspec/matchers/{cover_spec.rb → built_in/cover_spec.rb} +0 -0
  77. data/spec/rspec/matchers/built_in/eq_spec.rb +156 -0
  78. data/spec/rspec/matchers/{eql_spec.rb → built_in/eql_spec.rb} +2 -2
  79. data/spec/rspec/matchers/built_in/equal_spec.rb +106 -0
  80. data/spec/rspec/matchers/{exist_spec.rb → built_in/exist_spec.rb} +1 -1
  81. data/spec/rspec/matchers/{has_spec.rb → built_in/has_spec.rb} +39 -0
  82. data/spec/rspec/matchers/{include_spec.rb → built_in/include_spec.rb} +118 -109
  83. data/spec/rspec/matchers/{match_spec.rb → built_in/match_spec.rb} +30 -2
  84. data/spec/rspec/matchers/{operator_matcher_spec.rb → built_in/operators_spec.rb} +26 -26
  85. data/spec/rspec/matchers/built_in/output_spec.rb +165 -0
  86. data/spec/rspec/matchers/{raise_error_spec.rb → built_in/raise_error_spec.rb} +81 -11
  87. data/spec/rspec/matchers/{respond_to_spec.rb → built_in/respond_to_spec.rb} +0 -0
  88. data/spec/rspec/matchers/{satisfy_spec.rb → built_in/satisfy_spec.rb} +0 -0
  89. data/spec/rspec/matchers/{start_with_end_with_spec.rb → built_in/start_and_end_with_spec.rb} +82 -15
  90. data/spec/rspec/matchers/{throw_symbol_spec.rb → built_in/throw_symbol_spec.rb} +29 -10
  91. data/spec/rspec/matchers/{yield_spec.rb → built_in/yield_spec.rb} +90 -0
  92. data/spec/rspec/matchers/configuration_spec.rb +7 -39
  93. data/spec/rspec/matchers/description_generation_spec.rb +22 -6
  94. data/spec/rspec/matchers/dsl_spec.rb +838 -0
  95. data/spec/rspec/matchers/legacy_spec.rb +101 -0
  96. data/spec/rspec/matchers_spec.rb +74 -0
  97. data/spec/spec_helper.rb +35 -21
  98. data/spec/support/shared_examples.rb +26 -4
  99. metadata +172 -116
  100. metadata.gz.sig +3 -4
  101. checksums.yaml +0 -15
  102. checksums.yaml.gz.sig +0 -0
  103. data/features/built_in_matchers/match_array.feature +0 -37
  104. data/lib/rspec/expectations/errors.rb +0 -9
  105. data/lib/rspec/expectations/extensions.rb +0 -1
  106. data/lib/rspec/expectations/extensions/object.rb +0 -29
  107. data/lib/rspec/matchers/built_in/match_array.rb +0 -51
  108. data/lib/rspec/matchers/compatibility.rb +0 -14
  109. data/lib/rspec/matchers/matcher.rb +0 -301
  110. data/lib/rspec/matchers/method_missing.rb +0 -12
  111. data/lib/rspec/matchers/operator_matcher.rb +0 -99
  112. data/lib/rspec/matchers/test_unit_integration.rb +0 -11
  113. data/spec/rspec/matchers/eq_spec.rb +0 -60
  114. data/spec/rspec/matchers/equal_spec.rb +0 -78
  115. data/spec/rspec/matchers/include_matcher_integration_spec.rb +0 -30
  116. data/spec/rspec/matchers/match_array_spec.rb +0 -194
  117. data/spec/rspec/matchers/matcher_spec.rb +0 -706
  118. data/spec/rspec/matchers/matchers_spec.rb +0 -36
  119. data/spec/rspec/matchers/method_missing_spec.rb +0 -28
  120. data/spec/support/classes.rb +0 -56
  121. data/spec/support/in_sub_process.rb +0 -37
  122. data/spec/support/ruby_version.rb +0 -10
@@ -0,0 +1,292 @@
1
+ require 'spec_helper'
2
+
3
+ module RSpec::Matchers::BuiltIn
4
+ describe Compound do
5
+ context "when used as a composable matcher" do
6
+ it 'can pass' do
7
+ expect(["food", "barn"]).to include(
8
+ a_string_starting_with("f").and(ending_with("d")),
9
+ a_string_starting_with("b").and(ending_with("n"))
10
+ )
11
+ end
12
+
13
+ it 'can fail' do
14
+ expect {
15
+ expect(["foo", "bar"]).to include(
16
+ a_string_starting_with("f").and(ending_with("d")),
17
+ a_string_starting_with("b").and(ending_with("n"))
18
+ )
19
+ }.to fail_matching('expected ["foo", "bar"] to include (a string starting with "f" and ending with "d") and (a string starting with "b" and ending with "n")')
20
+ end
21
+
22
+ it 'provides a description' do
23
+ matcher = include(
24
+ a_string_starting_with("f").and(ending_with("d")),
25
+ a_string_starting_with("b").and(ending_with("n"))
26
+ )
27
+
28
+ expect(matcher.description).to eq('include (a string starting with "f" and ending with "d") and (a string starting with "b" and ending with "n")')
29
+ end
30
+ end
31
+
32
+ describe "expect(...).to matcher.and(other_matcher)" do
33
+
34
+ it_behaves_like "an RSpec matcher", :valid_value => 3, :invalid_value => 4 do
35
+ let(:matcher) { eq(3).and be <= 3 }
36
+ end
37
+
38
+ context 'when both matchers pass' do
39
+ it 'passes' do
40
+ expect(3).to eq(3).and be >= 2
41
+ end
42
+ end
43
+
44
+ it 'has a description composed of both matcher descriptions' do
45
+ matcher = eq(3).and be >= 2
46
+ expect(3).to matcher
47
+ expect(matcher.description).to eq("eq 3 and be >= 2")
48
+ end
49
+
50
+ context 'when only the first matcher fails' do
51
+ it "fails with the first matcher's failure message" do
52
+ expect {
53
+ expect(3).to eq(4).and be >= 2
54
+ }.to fail_with(dedent <<-EOS)
55
+ |
56
+ |expected: 4
57
+ | got: 3
58
+ |
59
+ |(compared using ==)
60
+ |
61
+ EOS
62
+ end
63
+ end
64
+
65
+ context 'when only the second matcher fails' do
66
+ it "fails with the second matcher's failure message" do
67
+ expect {
68
+ expect(3).to be_kind_of(Integer).and eq(4)
69
+ }.to fail_with(dedent <<-EOS)
70
+ |
71
+ |expected: 4
72
+ | got: 3
73
+ |
74
+ |(compared using ==)
75
+ |
76
+ EOS
77
+ end
78
+ end
79
+
80
+ context "when both mathers fail" do
81
+ context "when both matchers have multi-line failure messages" do
82
+ it 'fails with a well formatted message containing both sub-messages' do
83
+ expect {
84
+ expect(3).to eq(4).and be >= 8
85
+ }.to fail_with(dedent <<-EOS)
86
+ |
87
+ | expected: 4
88
+ | got: 3
89
+ |
90
+ | (compared using ==)
91
+ |
92
+ |...and:
93
+ |
94
+ | expected: >= 8
95
+ | got: 3
96
+ EOS
97
+ end
98
+ end
99
+
100
+ context "when both matchers have single-line failure messages" do
101
+ it 'fails with a single-line failure message' do
102
+ expect {
103
+ expect("foo").to start_with("a").and end_with("z")
104
+ }.to fail_with('expected "foo" to start with "a" and expected "foo" to end with "z"')
105
+ end
106
+ end
107
+
108
+ context "when the first matcher has a multi-line failure message" do
109
+ it 'fails with a well formatted message containing both sub-messages' do
110
+ expect {
111
+ expect("foo").to eq(4).and end_with("z")
112
+ }.to fail_with(dedent <<-EOS)
113
+ |
114
+ | expected: 4
115
+ | got: "foo"
116
+ |
117
+ | (compared using ==)
118
+ |
119
+ |...and:
120
+ |
121
+ | expected "foo" to end with "z"
122
+ EOS
123
+ end
124
+ end
125
+
126
+ context "when the second matcher has a multi-line failure message" do
127
+ it 'fails with a well formatted message containing both sub-messages' do
128
+ expect {
129
+ expect("foo").to end_with("z").and eq(4)
130
+ }.to fail_with(dedent <<-EOS)
131
+ | expected "foo" to end with "z"
132
+ |
133
+ |...and:
134
+ |
135
+ | expected: 4
136
+ | got: "foo"
137
+ |
138
+ | (compared using ==)
139
+ |
140
+ EOS
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ describe "expect(...).not_to matcher.and(other_matcher)" do
147
+ it "is not supported" do
148
+ expect {
149
+ expect(3).not_to eq(2).and be > 2
150
+ }.to raise_error(NotImplementedError, /matcher.and matcher` is not supported/)
151
+ end
152
+ end
153
+
154
+ describe "expect(...).to matcher.or(other_matcher)" do
155
+ it_behaves_like "an RSpec matcher", :valid_value => 3, :invalid_value => 5 do
156
+ let(:matcher) { eq(3).or eq(4) }
157
+ end
158
+
159
+ it 'has a description composed of both matcher descriptions' do
160
+ matcher = eq(3).or eq(4)
161
+ expect(3).to matcher
162
+ expect(matcher.description).to eq("eq 3 or eq 4")
163
+ end
164
+
165
+ context 'when both matchers pass' do
166
+ it 'passes' do
167
+ expect("foo").to start_with("f").or end_with("o")
168
+ end
169
+ end
170
+
171
+ context 'when only the first matcher passes' do
172
+ it 'passes' do
173
+ expect("foo").to start_with("f").or end_with("z")
174
+ end
175
+ end
176
+
177
+ context 'when only the last matcher passes' do
178
+ it 'passes' do
179
+ expect("foo").to start_with("a").or end_with("o")
180
+ end
181
+ end
182
+
183
+ context 'when both matchers fail' do
184
+ context "when both matchers have multi-line failure messages" do
185
+ it 'fails with a well formatted message containing both sub-messages' do
186
+ expect {
187
+ expect(3).to eq(4).or be >= 8
188
+ }.to fail_with(dedent <<-EOS)
189
+ |
190
+ | expected: 4
191
+ | got: 3
192
+ |
193
+ | (compared using ==)
194
+ |
195
+ |...or:
196
+ |
197
+ | expected: >= 8
198
+ | got: 3
199
+ EOS
200
+ end
201
+ end
202
+
203
+ context "when both matchers have single-line failure messages" do
204
+ it 'fails with a single-line failure message' do
205
+ expect {
206
+ expect("foo").to start_with("a").or end_with("z")
207
+ }.to fail_with('expected "foo" to start with "a" or expected "foo" to end with "z"')
208
+ end
209
+ end
210
+
211
+ context "when the first matcher has a multi-line failure message" do
212
+ it 'fails with a well formatted message containing both sub-messages' do
213
+ expect {
214
+ expect("foo").to eq(4).or end_with("z")
215
+ }.to fail_with(dedent <<-EOS)
216
+ |
217
+ | expected: 4
218
+ | got: "foo"
219
+ |
220
+ | (compared using ==)
221
+ |
222
+ |...or:
223
+ |
224
+ | expected "foo" to end with "z"
225
+ EOS
226
+ end
227
+ end
228
+
229
+ context "when the second matcher has a multi-line failure message" do
230
+ it 'fails with a well formatted message containing both sub-messages' do
231
+ expect {
232
+ expect("foo").to end_with("z").or eq(4)
233
+ }.to fail_with(dedent <<-EOS)
234
+ | expected "foo" to end with "z"
235
+ |
236
+ |...or:
237
+ |
238
+ | expected: 4
239
+ | got: "foo"
240
+ |
241
+ | (compared using ==)
242
+ |
243
+ EOS
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ context "when chaining many matchers together" do
250
+ it 'can pass appropriately' do
251
+ matcher = start_with("f").and end_with("z").or end_with("o")
252
+ expect("foo").to matcher
253
+ expect(matcher.description).to eq('start with "f" and end with "z" or end with "o"')
254
+ end
255
+
256
+ it 'fails with a complete message' do
257
+ expect {
258
+ expect(3).to eq(1).and eq(2).and eq(3).and eq(4)
259
+ }.to fail_with(dedent <<-EOS)
260
+ |
261
+ | expected: 1
262
+ | got: 3
263
+ |
264
+ | (compared using ==)
265
+ |
266
+ |...and:
267
+ |
268
+ | expected: 2
269
+ | got: 3
270
+ |
271
+ | (compared using ==)
272
+ |
273
+ | ...and:
274
+ |
275
+ | expected: 4
276
+ | got: 3
277
+ |
278
+ | (compared using ==)
279
+ |
280
+ EOS
281
+ end
282
+ end
283
+
284
+ describe "expect(...).not_to matcher.or(other_matcher)" do
285
+ it "is not supported" do
286
+ expect {
287
+ expect(3).not_to eq(2).or be > 2
288
+ }.to raise_error(NotImplementedError, /matcher.or matcher` is not supported/)
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,441 @@
1
+ require 'spec_helper'
2
+ require 'timeout'
3
+
4
+ class UnsortableObject
5
+ def initialize(id)
6
+ @id = id
7
+ end
8
+
9
+ def inspect
10
+ @id.to_s
11
+ end
12
+
13
+ def ==(other)
14
+ false
15
+ end
16
+ end
17
+
18
+ # AR::Relation is meant to act like a collection, but does
19
+ # not include `Enumerable`. It does implement `to_ary`.
20
+ class FakeActiveRecordRelation
21
+ def initialize(records)
22
+ @records = records
23
+ end
24
+
25
+ def to_ary
26
+ @records
27
+ end
28
+ end
29
+
30
+ describe "should =~ array", :uses_should do
31
+ it "passes a valid positive expectation" do
32
+ [1, 2].should =~ [2, 1]
33
+ end
34
+
35
+ it "fails an invalid positive expectation" do
36
+ expect {
37
+ [1, 2, 3].should =~ [2, 1]
38
+ }.to fail_with(/expected collection contained/)
39
+ end
40
+
41
+ context "when the array defines a `=~` method" do
42
+ it 'delegates to that method rather than using the contain_exactly matcher' do
43
+ array = []
44
+ def array.=~(other)
45
+ other == :foo
46
+ end
47
+
48
+ array.should =~ :foo
49
+ expect {
50
+ array.should =~ :bar
51
+ }.to fail_with(/expected: :bar/)
52
+ end
53
+ end
54
+
55
+ context 'when the array defines a `send` method' do
56
+ it 'still works' do
57
+ array = [1, 2]
58
+ def array.send; :sent; end
59
+
60
+ array.should =~ array
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "should_not =~ [:with, :multiple, :args]", :uses_should do
66
+ it "is not supported" do
67
+ expect {
68
+ [1,2,3].should_not =~ [1,2,3]
69
+ }.to fail_with(/`contain_exactly` does not support negation/)
70
+ end
71
+ end
72
+
73
+ describe "using contain_exactly with expect" do
74
+ it "passes a valid positive expectation" do
75
+ expect([1, 2]).to contain_exactly(2, 1)
76
+ end
77
+
78
+ it "fails an invalid positive expectation" do
79
+ expect {
80
+ expect([1, 2, 3]).to contain_exactly(2, 1)
81
+ }.to fail_with(/expected collection contained/)
82
+ end
83
+ end
84
+
85
+ describe "expect(array).to contain_exactly(*other_array)" do
86
+ it_behaves_like "an RSpec matcher", :valid_value => [1, 2], :invalid_value => [1] do
87
+ let(:matcher) { contain_exactly(2, 1) }
88
+ end
89
+
90
+ it 'is also exposed as `match_array` (with unsplatted args)' do
91
+ expect([1, 2, 3]).to match_array([3, 2, 1])
92
+ end
93
+
94
+ it "passes if target contains all items" do
95
+ expect([1,2,3]).to contain_exactly(1,2,3)
96
+ end
97
+
98
+ it "passes if target contains all items out of order" do
99
+ expect([1,3,2]).to contain_exactly(1,2,3)
100
+ end
101
+
102
+ def timeout_if_not_debugging(time)
103
+ return yield if defined?(::Debugger)
104
+ Timeout.timeout(time) { yield }
105
+ end
106
+
107
+ it 'fails a match of 11 items with duplicates in a reasonable amount of time' do
108
+ timeout_if_not_debugging(0.1) do
109
+ expected = [0, 1, 1, 3, 3, 3, 4, 4, 8, 8, 9 ]
110
+ actual = [ 1, 2, 3, 3, 3, 3, 7, 8, 8, 9, 9]
111
+
112
+ expect {
113
+ expect(actual).to contain_exactly(*expected)
114
+ }.to fail_matching("the missing elements were: [0, 1, 4, 4]")
115
+ end
116
+ end
117
+
118
+ it "fails if target includes extra items" do
119
+ expect {
120
+ expect([1,2,3,4]).to contain_exactly(1,2,3)
121
+ }.to fail_with(<<-MESSAGE)
122
+ expected collection contained: [1, 2, 3]
123
+ actual collection contained: [1, 2, 3, 4]
124
+ the extra elements were: [4]
125
+ MESSAGE
126
+ end
127
+
128
+ it "fails if target is missing items" do
129
+ expect {
130
+ expect([1,2]).to contain_exactly(1,2,3)
131
+ }.to fail_with(<<-MESSAGE)
132
+ expected collection contained: [1, 2, 3]
133
+ actual collection contained: [1, 2]
134
+ the missing elements were: [3]
135
+ MESSAGE
136
+ end
137
+
138
+ it "fails if target is missing items and has extra items" do
139
+ expect {
140
+ expect([1,2,4]).to contain_exactly(1,2,3)
141
+ }.to fail_with(<<-MESSAGE)
142
+ expected collection contained: [1, 2, 3]
143
+ actual collection contained: [1, 2, 4]
144
+ the missing elements were: [3]
145
+ the extra elements were: [4]
146
+ MESSAGE
147
+ end
148
+
149
+ it "sorts items in the error message if they all respond to <=>" do
150
+ expect {
151
+ expect([6,2,1,5]).to contain_exactly(4,1,2,3)
152
+ }.to fail_with(<<-MESSAGE)
153
+ expected collection contained: [1, 2, 3, 4]
154
+ actual collection contained: [1, 2, 5, 6]
155
+ the missing elements were: [3, 4]
156
+ the extra elements were: [5, 6]
157
+ MESSAGE
158
+ end
159
+
160
+ it "does not sort items in the error message if they don't all respond to <=>" do
161
+ expect {
162
+ expect([UnsortableObject.new(2), UnsortableObject.new(1)]).to contain_exactly(UnsortableObject.new(4), UnsortableObject.new(3))
163
+ }.to fail_with(<<-MESSAGE)
164
+ expected collection contained: [4, 3]
165
+ actual collection contained: [2, 1]
166
+ the missing elements were: [4, 3]
167
+ the extra elements were: [2, 1]
168
+ MESSAGE
169
+ end
170
+
171
+ it "accurately reports extra elements when there are duplicates" do
172
+ expect {
173
+ expect([1,1,1,5]).to contain_exactly(1,5)
174
+ }.to fail_with(<<-MESSAGE)
175
+ expected collection contained: [1, 5]
176
+ actual collection contained: [1, 1, 1, 5]
177
+ the extra elements were: [1, 1]
178
+ MESSAGE
179
+ end
180
+
181
+ it "accurately reports missing elements when there are duplicates" do
182
+ expect {
183
+ expect([1,5]).to contain_exactly(1,1,5)
184
+ }.to fail_with(<<-MESSAGE)
185
+ expected collection contained: [1, 1, 5]
186
+ actual collection contained: [1, 5]
187
+ the missing elements were: [1]
188
+ MESSAGE
189
+ end
190
+ end
191
+
192
+ describe "expect(...).not_to contain_exactly(:with, :multiple, :args]" do
193
+ it "is not supported" do
194
+ expect {
195
+ expect([1,2,3]).not_to contain_exactly(1,2,3)
196
+ }.to fail_with(/`contain_exactly` does not support negation/)
197
+ end
198
+ end
199
+
200
+ describe "matching against things that aren't arrays" do
201
+ it "fails with nil and the expected error message is given" do
202
+ expect {
203
+ expect(nil).to contain_exactly(1, 2, 3)
204
+ }.to fail_with(/expected a collection/)
205
+ end
206
+
207
+ it "fails with a float and the expected error message is given" do
208
+ expect {
209
+ expect(3.7).to contain_exactly(1, 2, 3)
210
+ }.to fail_with(/expected a collection/)
211
+ end
212
+
213
+ it "fails with a string and the expected error message is given" do
214
+ expect {
215
+ expect("I like turtles").to contain_exactly(1, 2, 3)
216
+ }.to fail_with(/expected a collection/)
217
+ end
218
+
219
+ it 'works with other collection types' do
220
+ expect(Set.new([3, 2, 1])).to contain_exactly(1, 2, 3)
221
+ expect {
222
+ expect(Set.new([3, 2, 1])).to contain_exactly(1, 2)
223
+ }.to fail_matching("expected collection contained: [1, 2]")
224
+ end
225
+
226
+ it 'works with non-enumerables that implement `to_ary`' do
227
+ relation = FakeActiveRecordRelation.new([1, 2, 3])
228
+ expect(relation).to contain_exactly(2, 1, 3)
229
+ expect {
230
+ expect(relation).to contain_exactly(1, 2)
231
+ }.to fail_matching("expected collection contained: [1, 2]")
232
+ end
233
+ end
234
+
235
+ describe "Composing `contain_exactly` with other matchers" do
236
+ describe "expect(...).to contain_exactly(matcher, matcher])" do
237
+ it 'passes when the array matches the matchers in the same order' do
238
+ expect(["food", "barn"]).to contain_exactly(
239
+ a_string_matching(/foo/),
240
+ a_string_matching(/bar/)
241
+ )
242
+ end
243
+
244
+ it 'passes when the array matches the matchers in a different order' do
245
+ expect(["food", "barn"]).to contain_exactly(
246
+ a_string_matching(/bar/),
247
+ a_string_matching(/foo/)
248
+ )
249
+ end
250
+
251
+ it 'fails with a useful message when there is an extra element' do
252
+ expect {
253
+ expect(["food", "barn", "goo"]).to contain_exactly(
254
+ a_string_matching(/bar/),
255
+ a_string_matching(/foo/)
256
+ )
257
+ }.to fail_with(dedent <<-EOS)
258
+ |expected collection contained: [(a string matching /bar/), (a string matching /foo/)]
259
+ |actual collection contained: ["barn", "food", "goo"]
260
+ |the extra elements were: ["goo"]
261
+ |
262
+ EOS
263
+ end
264
+
265
+ it 'fails with a useful message when there is a missing element' do
266
+ expect {
267
+ expect(["food", "barn"]).to contain_exactly(
268
+ a_string_matching(/bar/),
269
+ a_string_matching(/foo/),
270
+ a_string_matching(/goo/)
271
+ )
272
+ }.to fail_with(dedent <<-EOS)
273
+ |expected collection contained: [(a string matching /bar/), (a string matching /foo/), (a string matching /goo/)]
274
+ |actual collection contained: ["barn", "food"]
275
+ |the missing elements were: [(a string matching /goo/)]
276
+ |
277
+ EOS
278
+ end
279
+
280
+ it 'pairs up the items in order to minimize the number of unpaired items' do
281
+ expect {
282
+ expect(["fool", "food", "good"]).to contain_exactly(/foo/, /fool/, /poo/)
283
+ }.to fail_with(dedent <<-EOS)
284
+ |expected collection contained: [/foo/, /fool/, /poo/]
285
+ |actual collection contained: ["food", "fool", "good"]
286
+ |the missing elements were: [/poo/]
287
+ |the extra elements were: ["good"]
288
+ |
289
+ EOS
290
+ end
291
+
292
+ it 'provides a description' do
293
+ description = contain_exactly(a_string_matching(/bar/), a_string_matching(/foo/)).description
294
+ expect(description).to eq("contain exactly (a string matching /bar/) and (a string matching /foo/)")
295
+ end
296
+
297
+ context 'when an earlier matcher matches more strictly than a later matcher' do
298
+ it 'works when the actual items match in the same order' do
299
+ expect(["food", "fool"]).to contain_exactly(a_string_matching(/foo/), a_string_matching(/fool/))
300
+ end
301
+
302
+ it 'works when the actual items match in reverse order' do
303
+ expect(["fool", "food"]).to contain_exactly(a_string_matching(/foo/), a_string_matching(/fool/))
304
+ end
305
+
306
+ it 'can handle multiple sets of overlapping matches' do
307
+ expect(["fool", "barn", "bare", "food"]).to contain_exactly(
308
+ a_string_matching(/bar/),
309
+ a_string_matching(/barn/),
310
+ a_string_matching(/foo/),
311
+ a_string_matching(/fool/)
312
+ )
313
+ end
314
+ end
315
+
316
+ it "can use `a_value_within` and `a_string_starting_with` against multiple types of values" do
317
+ expect(["barn", 2.45]).to contain_exactly(
318
+ a_value_within(0.1).of(2.5),
319
+ a_string_starting_with("bar")
320
+ )
321
+ end
322
+
323
+ context 'when a later matcher matches more strictly than an earlier matcher' do
324
+ it 'works when the actual items match in the same order' do
325
+ expect(["fool", "food"]).to contain_exactly(a_string_matching(/fool/), a_string_matching(/foo/))
326
+ end
327
+
328
+ it 'works when the actual items match in reverse order' do
329
+ expect(["food", "fool"]).to contain_exactly(a_string_matching(/fool/), a_string_matching(/foo/))
330
+ end
331
+ end
332
+ end
333
+ end
334
+
335
+ module RSpec
336
+ module Matchers
337
+ module BuiltIn
338
+ class ContainExactly
339
+ describe PairingsMaximizer do
340
+ it 'finds unmatched expected indexes' do
341
+ maximizer = PairingsMaximizer.new({ 0 => [], 1 => [0] }, { 0 => [1] })
342
+ expect(maximizer.solution.unmatched_expected_indexes).to eq([0])
343
+ end
344
+
345
+ it 'finds unmatched actual indexes' do
346
+ maximizer = PairingsMaximizer.new({ 0 => [0] }, { 0 => [0], 1 => [] })
347
+ expect(maximizer.solution.unmatched_actual_indexes).to eq([1])
348
+ end
349
+
350
+ describe "finding indeterminate indexes" do
351
+ it 'does not include unmatched indexes' do
352
+ maximizer = PairingsMaximizer.new({ 0 => [], 1 => [0] }, { 0 => [1], 1 => [] })
353
+
354
+ expect(maximizer.solution.indeterminate_expected_indexes).not_to include(0)
355
+ expect(maximizer.solution.indeterminate_actual_indexes).not_to include(1)
356
+ end
357
+
358
+ it 'does not include indexes that are reciprocally to exactly one index' do
359
+ maximizer = PairingsMaximizer.new({ 0 => [], 1 => [0] }, { 0 => [1], 1 => [0] })
360
+
361
+ expect(maximizer.solution.indeterminate_expected_indexes).not_to include(1)
362
+ expect(maximizer.solution.indeterminate_actual_indexes).not_to include(0)
363
+ end
364
+
365
+ it 'includes indexes that have multiple matches' do
366
+ maximizer = PairingsMaximizer.new({ 0 => [0, 2], 1 => [0, 2], 2 => [] },
367
+ { 0 => [0, 1], 1 => [], 2 => [0, 1] })
368
+
369
+
370
+ expect(maximizer.solution.indeterminate_expected_indexes).to include(0, 1)
371
+ expect(maximizer.solution.indeterminate_actual_indexes).to include(0, 2)
372
+ end
373
+
374
+ it 'includes indexes that have one match which has multiple matches' do
375
+ maximizer = PairingsMaximizer.new({ 0 => [0], 1 => [0], 2 => [1, 2] },
376
+ { 0 => [0, 1], 1 => [2], 2 => [2] })
377
+
378
+ expect(maximizer.solution.indeterminate_expected_indexes).to include(0, 1)
379
+ expect(maximizer.solution.indeterminate_actual_indexes).to include(1, 2)
380
+ end
381
+ end
382
+
383
+ describe "#unmatched_item_count" do
384
+ it 'returns the count of unmatched items' do
385
+ maximizer = PairingsMaximizer.new({ 0 => [1], 1 => [0] },
386
+ { 0 => [1], 1 => [0] })
387
+ expect(maximizer.solution.unmatched_item_count).to eq(0)
388
+
389
+ maximizer = PairingsMaximizer.new({ 0 => [1], 1 => [0] },
390
+ { 0 => [1], 1 => [0], 2 => [] })
391
+ expect(maximizer.solution.unmatched_item_count).to eq(1)
392
+ end
393
+ end
394
+
395
+ describe "#find_best_solution" do
396
+ matcher :produce_result do |unmatched_expected, unmatched_actual|
397
+ match do |result|
398
+ result.candidate? &&
399
+ result.unmatched_expected_indexes == unmatched_expected &&
400
+ result.unmatched_actual_indexes == unmatched_actual
401
+ end
402
+
403
+ failure_message do |result|
404
+ if result.candidate_result?
405
+ "expected a complete solution, but still had indeterminate indexes: " +
406
+ "expected: #{result.indeterminate_expected_indexes.inspect}; " +
407
+ "actual: #{result.indeterminate_actual_indexes.inspect}"
408
+ elsif result.unmatched_expected_indexes != unmatched_expected
409
+ "expected unmatched_expected_indexes: #{unmatched_expected.inspect} " +
410
+ "but got: #{result.unmatched_expected_indexes.inspect}"
411
+ elsif result.unmatched_actual_indexes != unmatched_actual
412
+ "expected unmatched_actual_indexes: #{unmatched_actual.inspect} " +
413
+ "but got: #{result.unmatched_actual_indexes.inspect}"
414
+ end
415
+ end
416
+ end
417
+
418
+ it 'returns no unmatched indexes when everything reciprocally matches one item' do
419
+ maximizer = PairingsMaximizer.new({ 0 => [1], 1 => [0] },
420
+ { 0 => [1], 1 => [0] })
421
+ expect(maximizer.find_best_solution).to produce_result([], [])
422
+ end
423
+
424
+ it 'returns unmatched indexes for everything that has no matches' do
425
+ maximizer = PairingsMaximizer.new({ 0 => [], 1 => [0] },
426
+ { 0 => [1], 1 => [] })
427
+ expect(maximizer.find_best_solution).to produce_result([0], [1])
428
+ end
429
+
430
+ it 'searches the solution space for a perfectly matching solution' do
431
+ maximizer = PairingsMaximizer.new({ 0 => [0, 1], 1 => [0] },
432
+ { 0 => [0, 1], 1 => [0] })
433
+ expect(maximizer.find_best_solution).to produce_result([], [])
434
+ end
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
441
+