rspec-expectations 2.14.0 → 3.13.0
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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.document +1 -1
- data/.yardopts +1 -1
- data/Changelog.md +976 -25
- data/{License.txt → LICENSE.md} +5 -3
- data/README.md +162 -26
- data/lib/rspec/expectations/block_snippet_extractor.rb +253 -0
- data/lib/rspec/expectations/configuration.rb +230 -0
- data/lib/rspec/expectations/expectation_target.rb +127 -51
- data/lib/rspec/expectations/fail_with.rb +17 -57
- data/lib/rspec/expectations/failure_aggregator.rb +229 -0
- data/lib/rspec/expectations/handler.rb +146 -32
- data/lib/rspec/expectations/minitest_integration.rb +58 -0
- data/lib/rspec/expectations/syntax.rb +68 -100
- data/lib/rspec/expectations/version.rb +1 -1
- data/lib/rspec/expectations.rb +58 -23
- data/lib/rspec/matchers/aliased_matcher.rb +116 -0
- data/lib/rspec/matchers/built_in/all.rb +86 -0
- data/lib/rspec/matchers/built_in/base_matcher.rb +191 -20
- data/lib/rspec/matchers/built_in/be.rb +114 -114
- data/lib/rspec/matchers/built_in/be_between.rb +77 -0
- data/lib/rspec/matchers/built_in/be_instance_of.rb +15 -4
- data/lib/rspec/matchers/built_in/be_kind_of.rb +10 -1
- data/lib/rspec/matchers/built_in/be_within.rb +35 -18
- data/lib/rspec/matchers/built_in/change.rb +389 -80
- data/lib/rspec/matchers/built_in/compound.rb +290 -0
- data/lib/rspec/matchers/built_in/contain_exactly.rb +310 -0
- data/lib/rspec/matchers/built_in/count_expectation.rb +169 -0
- data/lib/rspec/matchers/built_in/cover.rb +3 -0
- data/lib/rspec/matchers/built_in/eq.rb +30 -8
- data/lib/rspec/matchers/built_in/eql.rb +23 -8
- data/lib/rspec/matchers/built_in/equal.rb +55 -22
- data/lib/rspec/matchers/built_in/exist.rb +74 -10
- data/lib/rspec/matchers/built_in/has.rb +141 -22
- data/lib/rspec/matchers/built_in/have_attributes.rb +114 -0
- data/lib/rspec/matchers/built_in/include.rb +184 -32
- data/lib/rspec/matchers/built_in/match.rb +95 -1
- data/lib/rspec/matchers/built_in/operators.rb +128 -0
- data/lib/rspec/matchers/built_in/output.rb +207 -0
- data/lib/rspec/matchers/built_in/raise_error.rb +192 -44
- data/lib/rspec/matchers/built_in/respond_to.rb +154 -28
- data/lib/rspec/matchers/built_in/satisfy.rb +39 -9
- data/lib/rspec/matchers/built_in/start_or_end_with.rb +94 -0
- data/lib/rspec/matchers/built_in/throw_symbol.rb +58 -14
- data/lib/rspec/matchers/built_in/yield.rb +240 -161
- data/lib/rspec/matchers/built_in.rb +47 -33
- data/lib/rspec/matchers/composable.rb +171 -0
- data/lib/rspec/matchers/dsl.rb +531 -10
- data/lib/rspec/matchers/english_phrasing.rb +58 -0
- data/lib/rspec/matchers/fail_matchers.rb +42 -0
- data/lib/rspec/matchers/generated_descriptions.rb +14 -8
- data/lib/rspec/matchers/matcher_delegator.rb +61 -0
- data/lib/rspec/matchers/matcher_protocol.rb +105 -0
- data/lib/rspec/matchers/multi_matcher_diff.rb +82 -0
- data/lib/rspec/matchers.rb +520 -173
- data.tar.gz.sig +0 -0
- metadata +141 -242
- metadata.gz.sig +2 -0
- data/features/README.md +0 -48
- data/features/Upgrade.md +0 -53
- data/features/built_in_matchers/README.md +0 -90
- data/features/built_in_matchers/be.feature +0 -175
- data/features/built_in_matchers/be_within.feature +0 -48
- data/features/built_in_matchers/cover.feature +0 -47
- data/features/built_in_matchers/end_with.feature +0 -48
- data/features/built_in_matchers/equality.feature +0 -139
- data/features/built_in_matchers/exist.feature +0 -45
- data/features/built_in_matchers/expect_change.feature +0 -59
- data/features/built_in_matchers/expect_error.feature +0 -144
- data/features/built_in_matchers/have.feature +0 -109
- data/features/built_in_matchers/include.feature +0 -174
- data/features/built_in_matchers/match.feature +0 -52
- data/features/built_in_matchers/operators.feature +0 -227
- data/features/built_in_matchers/predicates.feature +0 -137
- data/features/built_in_matchers/respond_to.feature +0 -84
- data/features/built_in_matchers/satisfy.feature +0 -33
- data/features/built_in_matchers/start_with.feature +0 -48
- data/features/built_in_matchers/throw_symbol.feature +0 -91
- data/features/built_in_matchers/types.feature +0 -116
- data/features/built_in_matchers/yield.feature +0 -161
- data/features/custom_matchers/access_running_example.feature +0 -53
- data/features/custom_matchers/define_diffable_matcher.feature +0 -27
- data/features/custom_matchers/define_matcher.feature +0 -368
- data/features/custom_matchers/define_matcher_outside_rspec.feature +0 -38
- data/features/custom_matchers/define_matcher_with_fluent_interface.feature +0 -24
- data/features/customized_message.feature +0 -22
- data/features/diffing.feature +0 -85
- data/features/implicit_docstrings.feature +0 -52
- data/features/step_definitions/additional_cli_steps.rb +0 -22
- data/features/support/env.rb +0 -14
- data/features/syntax_configuration.feature +0 -71
- data/features/test_frameworks/test_unit.feature +0 -44
- data/lib/rspec/expectations/deprecation.rb +0 -17
- data/lib/rspec/expectations/differ.rb +0 -133
- data/lib/rspec/expectations/errors.rb +0 -9
- data/lib/rspec/expectations/extensions/array.rb +0 -9
- data/lib/rspec/expectations/extensions/object.rb +0 -29
- data/lib/rspec/expectations/extensions.rb +0 -2
- data/lib/rspec/matchers/be_close.rb +0 -9
- data/lib/rspec/matchers/built_in/have.rb +0 -124
- data/lib/rspec/matchers/built_in/match_array.rb +0 -51
- data/lib/rspec/matchers/built_in/start_and_end_with.rb +0 -48
- data/lib/rspec/matchers/compatibility.rb +0 -14
- data/lib/rspec/matchers/configuration.rb +0 -108
- data/lib/rspec/matchers/extensions/instance_eval_with_args.rb +0 -39
- data/lib/rspec/matchers/matcher.rb +0 -300
- data/lib/rspec/matchers/method_missing.rb +0 -12
- data/lib/rspec/matchers/operator_matcher.rb +0 -109
- data/lib/rspec/matchers/pretty.rb +0 -70
- data/lib/rspec/matchers/test_unit_integration.rb +0 -11
- data/lib/rspec-expectations.rb +0 -1
- data/spec/rspec/expectations/differ_spec.rb +0 -192
- data/spec/rspec/expectations/expectation_target_spec.rb +0 -82
- data/spec/rspec/expectations/extensions/kernel_spec.rb +0 -67
- data/spec/rspec/expectations/fail_with_spec.rb +0 -114
- data/spec/rspec/expectations/handler_spec.rb +0 -227
- data/spec/rspec/expectations/syntax_spec.rb +0 -139
- data/spec/rspec/matchers/base_matcher_spec.rb +0 -62
- data/spec/rspec/matchers/be_close_spec.rb +0 -22
- data/spec/rspec/matchers/be_instance_of_spec.rb +0 -63
- data/spec/rspec/matchers/be_kind_of_spec.rb +0 -41
- data/spec/rspec/matchers/be_spec.rb +0 -516
- data/spec/rspec/matchers/be_within_spec.rb +0 -137
- data/spec/rspec/matchers/change_spec.rb +0 -553
- data/spec/rspec/matchers/configuration_spec.rb +0 -206
- data/spec/rspec/matchers/cover_spec.rb +0 -69
- data/spec/rspec/matchers/description_generation_spec.rb +0 -190
- data/spec/rspec/matchers/dsl_spec.rb +0 -57
- data/spec/rspec/matchers/eq_spec.rb +0 -60
- data/spec/rspec/matchers/eql_spec.rb +0 -41
- data/spec/rspec/matchers/equal_spec.rb +0 -78
- data/spec/rspec/matchers/exist_spec.rb +0 -124
- data/spec/rspec/matchers/has_spec.rb +0 -122
- data/spec/rspec/matchers/have_spec.rb +0 -455
- data/spec/rspec/matchers/include_matcher_integration_spec.rb +0 -30
- data/spec/rspec/matchers/include_spec.rb +0 -531
- data/spec/rspec/matchers/match_array_spec.rb +0 -194
- data/spec/rspec/matchers/match_spec.rb +0 -61
- data/spec/rspec/matchers/matcher_spec.rb +0 -471
- data/spec/rspec/matchers/matchers_spec.rb +0 -37
- data/spec/rspec/matchers/method_missing_spec.rb +0 -28
- data/spec/rspec/matchers/operator_matcher_spec.rb +0 -223
- data/spec/rspec/matchers/raise_error_spec.rb +0 -485
- data/spec/rspec/matchers/respond_to_spec.rb +0 -292
- data/spec/rspec/matchers/satisfy_spec.rb +0 -44
- data/spec/rspec/matchers/start_with_end_with_spec.rb +0 -186
- data/spec/rspec/matchers/throw_symbol_spec.rb +0 -116
- data/spec/rspec/matchers/yield_spec.rb +0 -514
- data/spec/spec_helper.rb +0 -54
- data/spec/support/classes.rb +0 -56
- data/spec/support/in_sub_process.rb +0 -38
- data/spec/support/matchers.rb +0 -22
- data/spec/support/ruby_version.rb +0 -10
- data/spec/support/shared_examples.rb +0 -13
@@ -0,0 +1,290 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Matchers
|
3
|
+
module BuiltIn
|
4
|
+
# @api private
|
5
|
+
# Base class for `and` and `or` compound matchers.
|
6
|
+
class Compound < BaseMatcher
|
7
|
+
# @private
|
8
|
+
attr_reader :matcher_1, :matcher_2, :evaluator
|
9
|
+
|
10
|
+
def initialize(matcher_1, matcher_2)
|
11
|
+
@matcher_1 = matcher_1
|
12
|
+
@matcher_2 = matcher_2
|
13
|
+
end
|
14
|
+
|
15
|
+
# @private
|
16
|
+
def does_not_match?(_actual)
|
17
|
+
raise NotImplementedError, "`expect(...).not_to matcher.#{conjunction} matcher` " \
|
18
|
+
"is not supported, since it creates a bit of an ambiguity. Instead, define negated versions " \
|
19
|
+
"of whatever matchers you wish to negate with `RSpec::Matchers.define_negated_matcher` and " \
|
20
|
+
"use `expect(...).to matcher.#{conjunction} matcher`."
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
# @return [String]
|
25
|
+
def description
|
26
|
+
"#{matcher_1.description} #{conjunction} #{matcher_2.description}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def supports_block_expectations?
|
31
|
+
matcher_supports_block_expectations?(matcher_1) &&
|
32
|
+
matcher_supports_block_expectations?(matcher_2)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def supports_value_expectations?
|
37
|
+
matcher_supports_value_expectations?(matcher_1) &&
|
38
|
+
matcher_supports_value_expectations?(matcher_2)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @api private
|
42
|
+
def expects_call_stack_jump?
|
43
|
+
NestedEvaluator.matcher_expects_call_stack_jump?(matcher_1) ||
|
44
|
+
NestedEvaluator.matcher_expects_call_stack_jump?(matcher_2)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
# @return [Boolean]
|
49
|
+
def diffable?
|
50
|
+
matcher_is_diffable?(matcher_1) || matcher_is_diffable?(matcher_2)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
# @return [RSpec::Matchers::MultiMatcherDiff]
|
55
|
+
def expected
|
56
|
+
return nil unless evaluator
|
57
|
+
::RSpec::Matchers::MultiMatcherDiff.for_many_matchers(diffable_matcher_list)
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def diffable_matcher_list
|
63
|
+
list = []
|
64
|
+
list.concat(diffable_matcher_list_for(matcher_1)) unless matcher_1_matches?
|
65
|
+
list.concat(diffable_matcher_list_for(matcher_2)) unless matcher_2_matches?
|
66
|
+
list
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def initialize_copy(other)
|
72
|
+
@matcher_1 = @matcher_1.clone
|
73
|
+
@matcher_2 = @matcher_2.clone
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def match(_expected, actual)
|
78
|
+
evaluator_klass = if supports_block_expectations? && Proc === actual
|
79
|
+
NestedEvaluator
|
80
|
+
else
|
81
|
+
SequentialEvaluator
|
82
|
+
end
|
83
|
+
|
84
|
+
@evaluator = evaluator_klass.new(actual, matcher_1, matcher_2)
|
85
|
+
end
|
86
|
+
|
87
|
+
def indent_multiline_message(message)
|
88
|
+
message.lines.map do |line|
|
89
|
+
line =~ /\S/ ? ' ' + line : line
|
90
|
+
end.join
|
91
|
+
end
|
92
|
+
|
93
|
+
def compound_failure_message
|
94
|
+
"#{indent_multiline_message(matcher_1.failure_message.sub(/\n+\z/, ''))}" \
|
95
|
+
"\n\n...#{conjunction}:" \
|
96
|
+
"\n\n#{indent_multiline_message(matcher_2.failure_message.sub(/\A\n+/, ''))}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def matcher_1_matches?
|
100
|
+
evaluator.matcher_matches?(matcher_1)
|
101
|
+
end
|
102
|
+
|
103
|
+
def matcher_2_matches?
|
104
|
+
evaluator.matcher_matches?(matcher_2)
|
105
|
+
end
|
106
|
+
|
107
|
+
def matcher_supports_block_expectations?(matcher)
|
108
|
+
matcher.supports_block_expectations?
|
109
|
+
rescue NoMethodError
|
110
|
+
false
|
111
|
+
end
|
112
|
+
|
113
|
+
def matcher_supports_value_expectations?(matcher)
|
114
|
+
matcher.supports_value_expectations?
|
115
|
+
rescue NoMethodError
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
def matcher_is_diffable?(matcher)
|
120
|
+
matcher.diffable?
|
121
|
+
rescue NoMethodError
|
122
|
+
false
|
123
|
+
end
|
124
|
+
|
125
|
+
def diffable_matcher_list_for(matcher)
|
126
|
+
return [] unless matcher_is_diffable?(matcher)
|
127
|
+
return matcher.diffable_matcher_list if Compound === matcher
|
128
|
+
[matcher]
|
129
|
+
end
|
130
|
+
|
131
|
+
# For value expectations, we can evaluate the matchers sequentially.
|
132
|
+
class SequentialEvaluator
|
133
|
+
def initialize(actual, *)
|
134
|
+
@actual = actual
|
135
|
+
end
|
136
|
+
|
137
|
+
def matcher_matches?(matcher)
|
138
|
+
matcher.matches?(@actual)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Normally, we evaluate the matching sequentially. For an expression like
|
143
|
+
# `expect(x).to foo.and bar`, this becomes:
|
144
|
+
#
|
145
|
+
# expect(x).to foo
|
146
|
+
# expect(x).to bar
|
147
|
+
#
|
148
|
+
# For block expectations, we need to nest them instead, so that
|
149
|
+
# `expect { x }.to foo.and bar` becomes:
|
150
|
+
#
|
151
|
+
# expect {
|
152
|
+
# expect { x }.to foo
|
153
|
+
# }.to bar
|
154
|
+
#
|
155
|
+
# This is necessary so that the `expect` block is only executed once.
|
156
|
+
class NestedEvaluator
|
157
|
+
def initialize(actual, matcher_1, matcher_2)
|
158
|
+
@actual = actual
|
159
|
+
@matcher_1 = matcher_1
|
160
|
+
@matcher_2 = matcher_2
|
161
|
+
@match_results = {}
|
162
|
+
|
163
|
+
inner, outer = order_block_matchers
|
164
|
+
|
165
|
+
@match_results[outer] = outer.matches?(Proc.new do |*args|
|
166
|
+
@match_results[inner] = inner.matches?(inner_matcher_block(args))
|
167
|
+
end)
|
168
|
+
end
|
169
|
+
|
170
|
+
def matcher_matches?(matcher)
|
171
|
+
@match_results.fetch(matcher) do
|
172
|
+
raise ArgumentError, "Your #{matcher.description} has no match " \
|
173
|
+
"results, this can occur when an unexpected call stack or " \
|
174
|
+
"local jump occurs. Perhaps one of your matchers needs to " \
|
175
|
+
"declare `expects_call_stack_jump?` as `true`?"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
# Some block matchers (such as `yield_xyz`) pass args to the `expect` block.
|
182
|
+
# When such a matcher is used as the outer matcher, we need to forward the
|
183
|
+
# the args on to the `expect` block.
|
184
|
+
def inner_matcher_block(outer_args)
|
185
|
+
return @actual if outer_args.empty?
|
186
|
+
|
187
|
+
Proc.new do |*inner_args|
|
188
|
+
unless inner_args.empty?
|
189
|
+
raise ArgumentError, "(#{@matcher_1.description}) and " \
|
190
|
+
"(#{@matcher_2.description}) cannot be combined in a compound expectation " \
|
191
|
+
"since both matchers pass arguments to the block."
|
192
|
+
end
|
193
|
+
|
194
|
+
@actual.call(*outer_args)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# For a matcher like `raise_error` or `throw_symbol`, where the block will jump
|
199
|
+
# up the call stack, we need to order things so that it is the inner matcher.
|
200
|
+
# For example, we need it to be this:
|
201
|
+
#
|
202
|
+
# expect {
|
203
|
+
# expect {
|
204
|
+
# x += 1
|
205
|
+
# raise "boom"
|
206
|
+
# }.to raise_error("boom")
|
207
|
+
# }.to change { x }.by(1)
|
208
|
+
#
|
209
|
+
# ...rather than:
|
210
|
+
#
|
211
|
+
# expect {
|
212
|
+
# expect {
|
213
|
+
# x += 1
|
214
|
+
# raise "boom"
|
215
|
+
# }.to change { x }.by(1)
|
216
|
+
# }.to raise_error("boom")
|
217
|
+
#
|
218
|
+
# In the latter case, the after-block logic in the `change` matcher would never
|
219
|
+
# get executed because the `raise "boom"` line would jump to the `rescue` in the
|
220
|
+
# `raise_error` logic, so only the former case will work properly.
|
221
|
+
#
|
222
|
+
# This method figures out which matcher should be the inner matcher and which
|
223
|
+
# should be the outer matcher.
|
224
|
+
def order_block_matchers
|
225
|
+
return @matcher_1, @matcher_2 unless self.class.matcher_expects_call_stack_jump?(@matcher_2)
|
226
|
+
return @matcher_2, @matcher_1 unless self.class.matcher_expects_call_stack_jump?(@matcher_1)
|
227
|
+
|
228
|
+
raise ArgumentError, "(#{@matcher_1.description}) and " \
|
229
|
+
"(#{@matcher_2.description}) cannot be combined in a compound expectation " \
|
230
|
+
"because they both expect a call stack jump."
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.matcher_expects_call_stack_jump?(matcher)
|
234
|
+
matcher.expects_call_stack_jump?
|
235
|
+
rescue NoMethodError
|
236
|
+
false
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# @api public
|
241
|
+
# Matcher used to represent a compound `and` expectation.
|
242
|
+
class And < self
|
243
|
+
# @api private
|
244
|
+
# @return [String]
|
245
|
+
def failure_message
|
246
|
+
if matcher_1_matches?
|
247
|
+
matcher_2.failure_message
|
248
|
+
elsif matcher_2_matches?
|
249
|
+
matcher_1.failure_message
|
250
|
+
else
|
251
|
+
compound_failure_message
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
def match(*)
|
258
|
+
super
|
259
|
+
matcher_1_matches? && matcher_2_matches?
|
260
|
+
end
|
261
|
+
|
262
|
+
def conjunction
|
263
|
+
"and"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# @api public
|
268
|
+
# Matcher used to represent a compound `or` expectation.
|
269
|
+
class Or < self
|
270
|
+
# @api private
|
271
|
+
# @return [String]
|
272
|
+
def failure_message
|
273
|
+
compound_failure_message
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
def match(*)
|
279
|
+
super
|
280
|
+
matcher_1_matches? || matcher_2_matches?
|
281
|
+
end
|
282
|
+
|
283
|
+
def conjunction
|
284
|
+
"or"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Matchers
|
3
|
+
module BuiltIn
|
4
|
+
# rubocop:disable Metrics/ClassLength
|
5
|
+
# @api private
|
6
|
+
# Provides the implementation for `contain_exactly` and `match_array`.
|
7
|
+
# Not intended to be instantiated directly.
|
8
|
+
class ContainExactly < BaseMatcher
|
9
|
+
# @api private
|
10
|
+
# @return [String]
|
11
|
+
def failure_message
|
12
|
+
if Array === actual
|
13
|
+
generate_failure_message
|
14
|
+
else
|
15
|
+
"expected a collection that can be converted to an array with " \
|
16
|
+
"`#to_ary` or `#to_a`, but got #{actual_formatted}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
# @return [String]
|
22
|
+
def failure_message_when_negated
|
23
|
+
list = EnglishPhrasing.list(surface_descriptions_in(expected))
|
24
|
+
"expected #{actual_formatted} not to contain exactly#{list}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
# @return [String]
|
29
|
+
def description
|
30
|
+
list = EnglishPhrasing.list(surface_descriptions_in(expected))
|
31
|
+
"contain exactly#{list}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def matches?(actual)
|
35
|
+
@pairings_maximizer = nil
|
36
|
+
@best_solution = nil
|
37
|
+
@extra_items = nil
|
38
|
+
@missing_items = nil
|
39
|
+
super(actual)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def generate_failure_message
|
45
|
+
message = expected_collection_line
|
46
|
+
message += actual_collection_line
|
47
|
+
message += missing_elements_line unless missing_items.empty?
|
48
|
+
message += extra_elements_line unless extra_items.empty?
|
49
|
+
message
|
50
|
+
end
|
51
|
+
|
52
|
+
def expected_collection_line
|
53
|
+
message_line('expected collection contained', expected, true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def actual_collection_line
|
57
|
+
message_line('actual collection contained', actual)
|
58
|
+
end
|
59
|
+
|
60
|
+
def missing_elements_line
|
61
|
+
message_line('the missing elements were', missing_items, true)
|
62
|
+
end
|
63
|
+
|
64
|
+
def extra_elements_line
|
65
|
+
message_line('the extra elements were', extra_items)
|
66
|
+
end
|
67
|
+
|
68
|
+
def describe_collection(collection, surface_descriptions=false)
|
69
|
+
if surface_descriptions
|
70
|
+
"#{description_of(safe_sort(surface_descriptions_in collection))}\n"
|
71
|
+
else
|
72
|
+
"#{description_of(safe_sort(collection))}\n"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def message_line(prefix, collection, surface_descriptions=false)
|
77
|
+
"%-32s%s" % [prefix + ':',
|
78
|
+
describe_collection(collection, surface_descriptions)]
|
79
|
+
end
|
80
|
+
|
81
|
+
def match(_expected, _actual)
|
82
|
+
return false unless convert_actual_to_an_array
|
83
|
+
match_when_sorted? || (extra_items.empty? && missing_items.empty?)
|
84
|
+
end
|
85
|
+
|
86
|
+
# This cannot always work (e.g. when dealing with unsortable items,
|
87
|
+
# or matchers as expected items), but it's practically free compared to
|
88
|
+
# the slowness of the full matching algorithm, and in common cases this
|
89
|
+
# works, so it's worth a try.
|
90
|
+
def match_when_sorted?
|
91
|
+
values_match?(safe_sort(expected), safe_sort(actual))
|
92
|
+
end
|
93
|
+
|
94
|
+
def convert_actual_to_an_array
|
95
|
+
if actual.respond_to?(:to_ary)
|
96
|
+
@actual = actual.to_ary
|
97
|
+
elsif actual.respond_to?(:to_a) && !to_a_disallowed?(actual)
|
98
|
+
@actual = actual.to_a
|
99
|
+
else
|
100
|
+
false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def safe_sort(array)
|
105
|
+
array.sort
|
106
|
+
rescue Support::AllExceptionsExceptOnesWeMustNotRescue
|
107
|
+
array
|
108
|
+
end
|
109
|
+
|
110
|
+
if RUBY_VERSION == "1.8.7"
|
111
|
+
def to_a_disallowed?(object)
|
112
|
+
case object
|
113
|
+
when NilClass, String then true
|
114
|
+
else Kernel == RSpec::Support.method_handle_for(object, :to_a).owner
|
115
|
+
end
|
116
|
+
end
|
117
|
+
else
|
118
|
+
def to_a_disallowed?(object)
|
119
|
+
NilClass === object
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def missing_items
|
124
|
+
@missing_items ||= best_solution.unmatched_expected_indexes.map do |index|
|
125
|
+
expected[index]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def extra_items
|
130
|
+
@extra_items ||= best_solution.unmatched_actual_indexes.map do |index|
|
131
|
+
actual[index]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def best_solution
|
136
|
+
@best_solution ||= pairings_maximizer.find_best_solution
|
137
|
+
end
|
138
|
+
|
139
|
+
def pairings_maximizer
|
140
|
+
@pairings_maximizer ||= begin
|
141
|
+
expected_matches = Hash[Array.new(expected.size) { |i| [i, []] }]
|
142
|
+
actual_matches = Hash[Array.new(actual.size) { |i| [i, []] }]
|
143
|
+
|
144
|
+
expected.each_with_index do |e, ei|
|
145
|
+
actual.each_with_index do |a, ai|
|
146
|
+
next unless values_match?(e, a)
|
147
|
+
|
148
|
+
expected_matches[ei] << ai
|
149
|
+
actual_matches[ai] << ei
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
PairingsMaximizer.new(expected_matches, actual_matches)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Once we started supporting composing matchers, the algorithm for this matcher got
|
158
|
+
# much more complicated. Consider this expression:
|
159
|
+
#
|
160
|
+
# expect(["fool", "food"]).to contain_exactly(/foo/, /fool/)
|
161
|
+
#
|
162
|
+
# This should pass (because we can pair /fool/ with "fool" and /foo/ with "food"), but
|
163
|
+
# the original algorithm used by this matcher would pair the first elements it could
|
164
|
+
# (/foo/ with "fool"), which would leave /fool/ and "food" unmatched. When we have
|
165
|
+
# an expected element which is a matcher that matches a superset of actual items
|
166
|
+
# compared to another expected element matcher, we need to consider every possible pairing.
|
167
|
+
#
|
168
|
+
# This class is designed to maximize the number of actual/expected pairings -- or,
|
169
|
+
# conversely, to minimize the number of unpaired items. It's essentially a brute
|
170
|
+
# force solution, but with a few heuristics applied to reduce the size of the
|
171
|
+
# problem space:
|
172
|
+
#
|
173
|
+
# * Any items which match none of the items in the other list are immediately
|
174
|
+
# placed into the `unmatched_expected_indexes` or `unmatched_actual_indexes` array.
|
175
|
+
# The extra items and missing items in the matcher failure message are derived
|
176
|
+
# from these arrays.
|
177
|
+
# * Any items which reciprocally match only each other are paired up and not
|
178
|
+
# considered further.
|
179
|
+
#
|
180
|
+
# What's left is only the items which match multiple items from the other list
|
181
|
+
# (or vice versa). From here, it performs a brute-force depth-first search,
|
182
|
+
# looking for a solution which pairs all elements in both lists, or, barring that,
|
183
|
+
# that produces the fewest unmatched items.
|
184
|
+
#
|
185
|
+
# @private
|
186
|
+
class PairingsMaximizer
|
187
|
+
# @private
|
188
|
+
Solution = Struct.new(:unmatched_expected_indexes, :unmatched_actual_indexes,
|
189
|
+
:indeterminate_expected_indexes, :indeterminate_actual_indexes) do
|
190
|
+
def worse_than?(other)
|
191
|
+
unmatched_item_count > other.unmatched_item_count
|
192
|
+
end
|
193
|
+
|
194
|
+
def candidate?
|
195
|
+
indeterminate_expected_indexes.empty? &&
|
196
|
+
indeterminate_actual_indexes.empty?
|
197
|
+
end
|
198
|
+
|
199
|
+
def ideal?
|
200
|
+
candidate? && (
|
201
|
+
unmatched_expected_indexes.empty? ||
|
202
|
+
unmatched_actual_indexes.empty?
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
def unmatched_item_count
|
207
|
+
unmatched_expected_indexes.count + unmatched_actual_indexes.count
|
208
|
+
end
|
209
|
+
|
210
|
+
def +(derived_candidate_solution)
|
211
|
+
self.class.new(
|
212
|
+
unmatched_expected_indexes + derived_candidate_solution.unmatched_expected_indexes,
|
213
|
+
unmatched_actual_indexes + derived_candidate_solution.unmatched_actual_indexes,
|
214
|
+
# Ignore the indeterminate indexes: by the time we get here,
|
215
|
+
# we've dealt with all indeterminates.
|
216
|
+
[], []
|
217
|
+
)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
attr_reader :expected_to_actual_matched_indexes, :actual_to_expected_matched_indexes, :solution
|
222
|
+
|
223
|
+
def initialize(expected_to_actual_matched_indexes, actual_to_expected_matched_indexes)
|
224
|
+
@expected_to_actual_matched_indexes = expected_to_actual_matched_indexes
|
225
|
+
@actual_to_expected_matched_indexes = actual_to_expected_matched_indexes
|
226
|
+
|
227
|
+
unmatched_expected_indexes, indeterminate_expected_indexes =
|
228
|
+
categorize_indexes(expected_to_actual_matched_indexes, actual_to_expected_matched_indexes)
|
229
|
+
|
230
|
+
unmatched_actual_indexes, indeterminate_actual_indexes =
|
231
|
+
categorize_indexes(actual_to_expected_matched_indexes, expected_to_actual_matched_indexes)
|
232
|
+
|
233
|
+
@solution = Solution.new(unmatched_expected_indexes, unmatched_actual_indexes,
|
234
|
+
indeterminate_expected_indexes, indeterminate_actual_indexes)
|
235
|
+
end
|
236
|
+
|
237
|
+
def find_best_solution
|
238
|
+
return solution if solution.candidate?
|
239
|
+
best_solution_so_far = NullSolution
|
240
|
+
|
241
|
+
expected_index = solution.indeterminate_expected_indexes.first
|
242
|
+
actuals = expected_to_actual_matched_indexes[expected_index]
|
243
|
+
|
244
|
+
actuals.each do |actual_index|
|
245
|
+
solution = best_solution_for_pairing(expected_index, actual_index)
|
246
|
+
return solution if solution.ideal?
|
247
|
+
best_solution_so_far = solution if best_solution_so_far.worse_than?(solution)
|
248
|
+
end
|
249
|
+
|
250
|
+
best_solution_so_far
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
# @private
|
256
|
+
# Starting solution that is worse than any other real solution.
|
257
|
+
NullSolution = Class.new do
|
258
|
+
def self.worse_than?(_other)
|
259
|
+
true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def categorize_indexes(indexes_to_categorize, other_indexes)
|
264
|
+
unmatched = []
|
265
|
+
indeterminate = []
|
266
|
+
|
267
|
+
indexes_to_categorize.each_pair do |index, matches|
|
268
|
+
if matches.empty?
|
269
|
+
unmatched << index
|
270
|
+
elsif !reciprocal_single_match?(matches, index, other_indexes)
|
271
|
+
indeterminate << index
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
return unmatched, indeterminate
|
276
|
+
end
|
277
|
+
|
278
|
+
def reciprocal_single_match?(matches, index, other_list)
|
279
|
+
return false unless matches.one?
|
280
|
+
other_list[matches.first] == [index]
|
281
|
+
end
|
282
|
+
|
283
|
+
def best_solution_for_pairing(expected_index, actual_index)
|
284
|
+
modified_expecteds = apply_pairing_to(
|
285
|
+
solution.indeterminate_expected_indexes,
|
286
|
+
expected_to_actual_matched_indexes, actual_index)
|
287
|
+
|
288
|
+
modified_expecteds.delete(expected_index)
|
289
|
+
|
290
|
+
modified_actuals = apply_pairing_to(
|
291
|
+
solution.indeterminate_actual_indexes,
|
292
|
+
actual_to_expected_matched_indexes, expected_index)
|
293
|
+
|
294
|
+
modified_actuals.delete(actual_index)
|
295
|
+
|
296
|
+
solution + self.class.new(modified_expecteds, modified_actuals).find_best_solution
|
297
|
+
end
|
298
|
+
|
299
|
+
def apply_pairing_to(indeterminates, original_matches, other_list_index)
|
300
|
+
indeterminates.inject({}) do |accum, index|
|
301
|
+
accum[index] = original_matches[index] - [other_list_index]
|
302
|
+
accum
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
# rubocop:enable Metrics/ClassLength
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|