rspec-expectations 3.1.2 → 3.2.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.
@@ -24,7 +24,7 @@ module RSpec
24
24
  autoload :Compound, 'rspec/matchers/built_in/compound'
25
25
  autoload :ContainExactly, 'rspec/matchers/built_in/contain_exactly'
26
26
  autoload :Cover, 'rspec/matchers/built_in/cover'
27
- autoload :EndWith, 'rspec/matchers/built_in/start_and_end_with'
27
+ autoload :EndWith, 'rspec/matchers/built_in/start_or_end_with'
28
28
  autoload :Eq, 'rspec/matchers/built_in/eq'
29
29
  autoload :Eql, 'rspec/matchers/built_in/eql'
30
30
  autoload :Equal, 'rspec/matchers/built_in/equal'
@@ -41,7 +41,7 @@ module RSpec
41
41
  autoload :RaiseError, 'rspec/matchers/built_in/raise_error'
42
42
  autoload :RespondTo, 'rspec/matchers/built_in/respond_to'
43
43
  autoload :Satisfy, 'rspec/matchers/built_in/satisfy'
44
- autoload :StartWith, 'rspec/matchers/built_in/start_and_end_with'
44
+ autoload :StartWith, 'rspec/matchers/built_in/start_or_end_with'
45
45
  autoload :ThrowSymbol, 'rspec/matchers/built_in/throw_symbol'
46
46
  autoload :YieldControl, 'rspec/matchers/built_in/yield'
47
47
  autoload :YieldSuccessiveArgs, 'rspec/matchers/built_in/yield'
@@ -215,7 +215,7 @@ module RSpec
215
215
  private
216
216
 
217
217
  def predicate_accessible?
218
- !private_predicate? && predicate_exists?
218
+ actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
219
219
  end
220
220
 
221
221
  # support 1.8.7, evaluate once at load time for performance
@@ -229,10 +229,6 @@ module RSpec
229
229
  end
230
230
  end
231
231
 
232
- def predicate_exists?
233
- actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
234
- end
235
-
236
232
  def predicate_matches?
237
233
  method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
238
234
  @predicate_matches = actual.__send__(method_name, *@args, &@block)
@@ -265,11 +261,11 @@ module RSpec
265
261
  end
266
262
 
267
263
  def validity_message
268
- if private_predicate?
269
- "expected #{@actual} to respond to `#{predicate}` but `#{predicate}` is a private method"
270
- elsif !predicate_exists?
271
- "expected #{@actual} to respond to `#{predicate}`"
272
- end
264
+ return nil if predicate_accessible?
265
+
266
+ msg = "expected #{@actual} to respond to `#{predicate}`"
267
+ msg << " but `#{predicate}` is a private method" if private_predicate?
268
+ msg
273
269
  end
274
270
  end
275
271
  end
@@ -3,9 +3,10 @@ module RSpec
3
3
  module BuiltIn
4
4
  # @api private
5
5
  # Base class for `and` and `or` compound matchers.
6
+ # rubocop:disable ClassLength
6
7
  class Compound < BaseMatcher
7
8
  # @private
8
- attr_reader :matcher_1, :matcher_2
9
+ attr_reader :matcher_1, :matcher_2, :evaluator
9
10
 
10
11
  def initialize(matcher_1, matcher_2)
11
12
  @matcher_1 = matcher_1
@@ -36,6 +37,28 @@ module RSpec
36
37
  NestedEvaluator.matcher_expects_call_stack_jump?(matcher_2)
37
38
  end
38
39
 
40
+ # @api private
41
+ # @return [Boolean]
42
+ def diffable?
43
+ matcher_is_diffable?(matcher_1) || matcher_is_diffable?(matcher_2)
44
+ end
45
+
46
+ # @api private
47
+ # @return [RSpec::Matchers::ExpectedsForMultipleDiffs]
48
+ def expected
49
+ return nil unless evaluator
50
+ ::RSpec::Matchers::ExpectedsForMultipleDiffs.for_many_matchers(diffable_matcher_list)
51
+ end
52
+
53
+ protected
54
+
55
+ def diffable_matcher_list
56
+ list = []
57
+ list.concat(diffable_matcher_list_for(matcher_1)) unless matcher_1_matches?
58
+ list.concat(diffable_matcher_list_for(matcher_2)) unless matcher_2_matches?
59
+ list
60
+ end
61
+
39
62
  private
40
63
 
41
64
  def initialize_copy(other)
@@ -88,11 +111,11 @@ module RSpec
88
111
  end
89
112
 
90
113
  def matcher_1_matches?
91
- @evaluator.matcher_matches?(matcher_1)
114
+ evaluator.matcher_matches?(matcher_1)
92
115
  end
93
116
 
94
117
  def matcher_2_matches?
95
- @evaluator.matcher_matches?(matcher_2)
118
+ evaluator.matcher_matches?(matcher_2)
96
119
  end
97
120
 
98
121
  def matcher_supports_block_expectations?(matcher)
@@ -101,6 +124,18 @@ module RSpec
101
124
  false
102
125
  end
103
126
 
127
+ def matcher_is_diffable?(matcher)
128
+ matcher.diffable?
129
+ rescue NoMethodError
130
+ false
131
+ end
132
+
133
+ def diffable_matcher_list_for(matcher)
134
+ return [] unless matcher_is_diffable?(matcher)
135
+ return matcher.diffable_matcher_list if Compound === matcher
136
+ [matcher]
137
+ end
138
+
104
139
  # For value expectations, we can evaluate the matchers sequentially.
105
140
  class SequentialEvaluator
106
141
  def initialize(actual, *)
@@ -10,13 +10,21 @@ module RSpec
10
10
 
11
11
  def initialize(expected)
12
12
  @expected = expected
13
+ @values = {}
13
14
  @respond_to_failed = false
15
+ @negated = false
16
+ end
17
+
18
+ # @private
19
+ def actual
20
+ @values
14
21
  end
15
22
 
16
23
  # @api private
17
24
  # @return [Boolean]
18
25
  def matches?(actual)
19
26
  @actual = actual
27
+ @negated = false
20
28
  return false unless respond_to_attributes?
21
29
  perform_match(:all?)
22
30
  end
@@ -25,6 +33,7 @@ module RSpec
25
33
  # @return [Boolean]
26
34
  def does_not_match?(actual)
27
35
  @actual = actual
36
+ @negated = true
28
37
  return false unless respond_to_attributes?
29
38
  perform_match(:none?)
30
39
  end
@@ -36,33 +45,49 @@ module RSpec
36
45
  improve_hash_formatting "have attributes #{described_items.inspect}"
37
46
  end
38
47
 
48
+ # @api private
49
+ # @return [Boolean]
50
+ def diffable?
51
+ !@respond_to_failed && !@negated
52
+ end
53
+
39
54
  # @api private
40
55
  # @return [String]
41
56
  def failure_message
42
- respond_to_failure_message_or { super }
57
+ respond_to_failure_message_or do
58
+ "expected #{@actual.inspect} to #{description} but had attributes #{ formatted_values }"
59
+ end
43
60
  end
44
61
 
45
62
  # @api private
46
63
  # @return [String]
47
64
  def failure_message_when_negated
48
- respond_to_failure_message_or { super }
65
+ respond_to_failure_message_or { "expected #{@actual.inspect} not to #{description}" }
49
66
  end
50
67
 
51
68
  private
52
69
 
70
+ def cache_all_values
71
+ @values = {}
72
+ expected.each do |attribute_key, _attribute_value|
73
+ actual_value = @actual.__send__(attribute_key)
74
+ @values[attribute_key] = actual_value
75
+ end
76
+ end
77
+
53
78
  def perform_match(predicate)
79
+ cache_all_values
54
80
  expected.__send__(predicate) do |attribute_key, attribute_value|
55
81
  actual_has_attribute?(attribute_key, attribute_value)
56
82
  end
57
83
  end
58
84
 
59
85
  def actual_has_attribute?(attribute_key, attribute_value)
60
- actual_value = actual.__send__(attribute_key)
61
- values_match?(attribute_value, actual_value)
86
+ values_match?(attribute_value, @values.fetch(attribute_key))
62
87
  end
63
88
 
64
89
  def respond_to_attributes?
65
- matches = respond_to_matcher.matches?(actual)
90
+ matches = respond_to_matcher.matches?(@actual)
66
91
  @respond_to_failed = !matches
67
92
  matches
68
93
  end
@@ -78,6 +103,10 @@ module RSpec
78
103
  improve_hash_formatting(yield)
79
104
  end
80
105
  end
106
+
107
+ def formatted_values
108
+ improve_hash_formatting(@values.inspect)
109
+ end
81
110
  end
82
111
  end
83
112
  end
@@ -33,13 +33,13 @@ module RSpec
33
33
  # @api private
34
34
  # @return [String]
35
35
  def failure_message
36
- improve_hash_formatting(super) + invalid_type_message
36
+ improve_hash_formatting(super) + invalid_object_message
37
37
  end
38
38
 
39
39
  # @api private
40
40
  # @return [String]
41
41
  def failure_message_when_negated
42
- improve_hash_formatting(super) + invalid_type_message
42
+ improve_hash_formatting(super) + invalid_object_message
43
43
  end
44
44
 
45
45
  # @api private
@@ -50,7 +50,7 @@ module RSpec
50
50
 
51
51
  private
52
52
 
53
- def invalid_type_message
53
+ def invalid_object_message
54
54
  return '' if actual.respond_to?(:include?)
55
55
  ", but it does not respond to `include?`"
56
56
  end
@@ -1,4 +1,5 @@
1
1
  require 'stringio'
2
+ require 'tempfile'
2
3
 
3
4
  module RSpec
4
5
  module Matchers
@@ -27,6 +28,7 @@ module RSpec
27
28
 
28
29
  # @api public
29
30
  # Tells the matcher to match against stdout.
31
+ # Works only when the main Ruby process prints to stdout
30
32
  def to_stdout
31
33
  @stream_capturer = CaptureStdout
32
34
  self
@@ -34,11 +36,30 @@ module RSpec
34
36
 
35
37
  # @api public
36
38
  # Tells the matcher to match against stderr.
39
+ # Works only when the main Ruby process prints to stderr
37
40
  def to_stderr
38
41
  @stream_capturer = CaptureStderr
39
42
  self
40
43
  end
41
44
 
45
+ # @api public
46
+ # Tells the matcher to match against stdout.
47
+ # Works when subprocesses print to stdout as well.
48
+ # This is significantly (~30x) slower than `to_stdout`
49
+ def to_stdout_from_any_process
50
+ @stream_capturer = CaptureStreamToTempfile.new("stdout", $stdout)
51
+ self
52
+ end
53
+
54
+ # @api public
55
+ # Tells the matcher to match against stderr.
56
+ # Works when subprocesses print to stderr as well.
57
+ # This is significantly (~30x) slower than `to_stderr`
58
+ def to_stderr_from_any_process
59
+ @stream_capturer = CaptureStreamToTempfile.new("stderr", $stderr)
60
+ self
61
+ end
62
+
42
63
  # @api private
43
64
  # @return [String]
44
65
  def failure_message
@@ -147,6 +168,26 @@ module RSpec
147
168
  $stderr = original_stream
148
169
  end
149
170
  end
171
+
172
+ # @private
173
+ class CaptureStreamToTempfile < Struct.new(:name, :stream)
174
+ def capture(block)
175
+ original_stream = stream.clone
176
+ captured_stream = Tempfile.new(name)
177
+
178
+ begin
179
+ captured_stream.sync = true
180
+ stream.reopen(captured_stream)
181
+ block.call
182
+ captured_stream.rewind
183
+ captured_stream.read
184
+ ensure
185
+ stream.reopen(original_stream)
186
+ captured_stream.close
187
+ captured_stream.unlink
188
+ end
189
+ end
190
+ end
150
191
  end
151
192
  end
152
193
  end
@@ -4,7 +4,7 @@ module RSpec
4
4
  # @api private
5
5
  # Base class for the `end_with` and `start_with` matchers.
6
6
  # Not intended to be instantiated directly.
7
- class StartAndEndWith < BaseMatcher
7
+ class StartOrEndWith < BaseMatcher
8
8
  def initialize(*expected)
9
9
  @actual_does_not_have_ordered_elements = false
10
10
  @expected = expected.length == 1 ? expected.first : expected
@@ -31,11 +31,11 @@ module RSpec
31
31
 
32
32
  private
33
33
 
34
- def match(expected, actual)
34
+ def match(_expected, actual)
35
35
  return false unless actual.respond_to?(:[])
36
36
 
37
37
  begin
38
- return subset_matches? if !(Struct === expected) && expected.respond_to?(:length)
38
+ return true if subsets_comparable? && subset_matches?
39
39
  element_matches?
40
40
  rescue ArgumentError
41
41
  @actual_does_not_have_ordered_elements = true
@@ -43,15 +43,25 @@ module RSpec
43
43
  end
44
44
  end
45
45
 
46
- def actual_is_unordered
47
- ArgumentError.new("#{actual.inspect} does not have ordered elements")
46
+ def subsets_comparable?
47
+ # Structs support the Enumerable interface but don't really have
48
+ # the semantics of a subset of a larger set...
49
+ return false if Struct === expected
50
+
51
+ expected.respond_to?(:length)
48
52
  end
49
53
  end
50
54
 
55
+ # For RSpec 3.1, the base class was named `StartAndEndWith`. For SemVer reasons,
56
+ # we still provide this constant until 4.0.
57
+ # @deprecated Use StartOrEndWith instead.
58
+ # @private
59
+ StartAndEndWith = StartOrEndWith
60
+
51
61
  # @api private
52
62
  # Provides the implementation for `start_with`.
53
63
  # Not intended to be instantiated directly.
54
- class StartWith < StartAndEndWith
64
+ class StartWith < StartOrEndWith
55
65
  private
56
66
 
57
67
  def subset_matches?
@@ -66,7 +76,7 @@ module RSpec
66
76
  # @api private
67
77
  # Provides the implementation for `end_with`.
68
78
  # Not intended to be instantiated directly.
69
- class EndWith < StartAndEndWith
79
+ class EndWith < StartOrEndWith
70
80
  private
71
81
 
72
82
  def subset_matches?
@@ -94,8 +94,7 @@ module RSpec
94
94
  # Not intended to be instantiated directly.
95
95
  class YieldControl < BaseMatcher
96
96
  def initialize
97
- @expectation_type = nil
98
- @expected_yields_count = nil
97
+ at_least(:once)
99
98
  end
100
99
 
101
100
  # @api public
@@ -151,11 +150,7 @@ module RSpec
151
150
  @probe = YieldProbe.probe(block)
152
151
  return false unless @probe.has_block?
153
152
 
154
- if @expectation_type
155
- @probe.num_yields.__send__(@expectation_type, @expected_yields_count)
156
- else
157
- @probe.yielded_once?(:yield_control)
158
- end
153
+ @probe.num_yields.__send__(@expectation_type, @expected_yields_count)
159
154
  end
160
155
 
161
156
  # @private
@@ -195,10 +190,11 @@ module RSpec
195
190
  def failure_reason
196
191
  return " but was not a block" unless @probe.has_block?
197
192
  return '' unless @expected_yields_count
198
- " #{human_readable_expecation_type}#{human_readable_count}"
193
+ " #{human_readable_expectation_type}#{human_readable_count(@expected_yields_count)}" \
194
+ " but yielded #{human_readable_count(@probe.num_yields)}"
199
195
  end
200
196
 
201
- def human_readable_expecation_type
197
+ def human_readable_expectation_type
202
198
  case @expectation_type
203
199
  when :<= then 'at most '
204
200
  when :>= then 'at least '
@@ -206,11 +202,11 @@ module RSpec
206
202
  end
207
203
  end
208
204
 
209
- def human_readable_count
210
- case @expected_yields_count
205
+ def human_readable_count(count)
206
+ case count
211
207
  when 1 then "once"
212
208
  when 2 then "twice"
213
- else "#{@expected_yields_count} times"
209
+ else "#{count} times"
214
210
  end
215
211
  end
216
212
  end
@@ -5,12 +5,30 @@ module RSpec
5
5
  # Defines a custom matcher.
6
6
  # @see RSpec::Matchers
7
7
  def define(name, &declarations)
8
- define_method name do |*expected|
9
- RSpec::Matchers::DSL::Matcher.new(name, declarations, self, *expected)
8
+ warn_about_block_args(name, declarations)
9
+ define_method name do |*expected, &block_arg|
10
+ RSpec::Matchers::DSL::Matcher.new(name, declarations, self, *expected, &block_arg)
10
11
  end
11
12
  end
12
13
  alias_method :matcher, :define
13
14
 
15
+ private
16
+
17
+ if Proc.method_defined?(:parameters)
18
+ def warn_about_block_args(name, declarations)
19
+ declarations.parameters.each do |type, arg_name|
20
+ next unless type == :block
21
+ RSpec.warning("Your `#{name}` custom matcher receives a block argument (`#{arg_name}`), " \
22
+ "but due to limitations in ruby, RSpec cannot provide the block. Instead, " \
23
+ "use the `block_arg` method to access the block")
24
+ end
25
+ end
26
+ else
27
+ def warn_about_block_args(*)
28
+ # There's no way to detect block params on 1.8 since the method reflection APIs don't expose it
29
+ end
30
+ end
31
+
14
32
  RSpec.configure { |c| c.extend self } if RSpec.respond_to?(:configure)
15
33
 
16
34
  # Contains the methods that are available from within the
@@ -165,6 +183,12 @@ module RSpec
165
183
  # hash been enabled, the chained method name and args will be added to the
166
184
  # default description and failure message.
167
185
  #
186
+ # In the common case where you just want the chained method to store some
187
+ # value(s) for later use (e.g. in `match`), you can provide one or more
188
+ # attribute names instead of a block; the chained method will store its
189
+ # arguments in instance variables with those names, and the values will
190
+ # be exposed via getters.
191
+ #
168
192
  # @example
169
193
  #
170
194
  # RSpec::Matchers.define :have_errors_on do |key|
@@ -178,19 +202,40 @@ module RSpec
178
202
  # end
179
203
  #
180
204
  # expect(minor).to have_errors_on(:age).with("Not old enough to participate")
181
- def chain(name, &definition)
182
- define_user_override(name, definition) do |*args, &block|
205
+ def chain(method_name, *attr_names, &definition)
206
+ unless block_given? ^ attr_names.any?
207
+ raise ArgumentError, "You must pass either a block or some attribute names (but not both) to `chain`."
208
+ end
209
+
210
+ definition = assign_attributes(attr_names) if attr_names.any?
211
+
212
+ define_user_override(method_name, definition) do |*args, &block|
183
213
  super(*args, &block)
184
- @chained_method_clauses.push([name, args])
214
+ @chained_method_clauses.push([method_name, args])
185
215
  self
186
216
  end
187
217
  end
188
218
 
219
+ def assign_attributes(attr_names)
220
+ attr_reader(*attr_names)
221
+ private(*attr_names)
222
+
223
+ lambda do |*attr_values|
224
+ attr_names.zip(attr_values) do |attr_name, attr_value|
225
+ instance_variable_set(:"@#{attr_name}", attr_value)
226
+ end
227
+ end
228
+ end
229
+
230
+ # assign_attributes isn't defined in the private section below because
231
+ # that makes MRI 1.9.2 emit a warning about private attributes.
232
+ private :assign_attributes
233
+
189
234
  private
190
235
 
191
236
  # Does the following:
192
237
  #
193
- # - Defines the named method usign a user-provided block
238
+ # - Defines the named method using a user-provided block
194
239
  # in @user_method_defs, which is included as an ancestor
195
240
  # in the singleton class in which we eval the `define` block.
196
241
  # - Defines an overriden definition for the same method
@@ -309,13 +354,17 @@ module RSpec
309
354
  # Could be useful to extract details for a failure message.
310
355
  attr_reader :rescued_exception
311
356
 
357
+ # The block parameter used in the expectation
358
+ attr_reader :block_arg
359
+
312
360
  # @api private
313
- def initialize(name, declarations, matcher_execution_context, *expected)
361
+ def initialize(name, declarations, matcher_execution_context, *expected, &block_arg)
314
362
  @name = name
315
363
  @actual = nil
316
364
  @expected_as_array = expected
317
365
  @matcher_execution_context = matcher_execution_context
318
366
  @chained_method_clauses = []
367
+ @block_arg = block_arg
319
368
 
320
369
  class << self
321
370
  # See `Macros#define_user_override` above, for an explanation.