rspec-expectations 3.9.3 → 3.10.2

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.
@@ -2,20 +2,15 @@ module RSpec
2
2
  module Matchers
3
3
  module BuiltIn
4
4
  # @api private
5
- # Provides the implementation for `has_<predicate>`.
6
- # Not intended to be instantiated directly.
7
- class Has < BaseMatcher
8
- if RSpec::Support::RubyFeatures.kw_args_supported?
9
- binding.eval(<<-CODE, __FILE__, __LINE__)
10
- def initialize(method_name, *args, **kwargs, &block)
11
- @method_name, @args, @kwargs, @block = method_name, args, kwargs, block
12
- end
13
- CODE
14
- else
15
- def initialize(method_name, *args, &block)
16
- @method_name, @args, @block = method_name, args, block
17
- end
5
+ # Provides the implementation for dynamic predicate matchers.
6
+ # Not intended to be inherited directly.
7
+ class DynamicPredicate < BaseMatcher
8
+ include BeHelpers
9
+
10
+ def initialize(method_name, *args, &block)
11
+ @method_name, @args, @block = method_name, args, block
18
12
  end
13
+ ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
19
14
 
20
15
  # @private
21
16
  def matches?(actual, &block)
@@ -28,31 +23,31 @@ module RSpec
28
23
  def does_not_match?(actual, &block)
29
24
  @actual = actual
30
25
  @block ||= block
31
- predicate_accessible? && !predicate_matches?
26
+ predicate_accessible? && predicate_matches?(false)
32
27
  end
33
28
 
34
29
  # @api private
35
30
  # @return [String]
36
31
  def failure_message
37
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return true, got false"
32
+ failure_message_expecting(true)
38
33
  end
39
34
 
40
35
  # @api private
41
36
  # @return [String]
42
37
  def failure_message_when_negated
43
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return false, got true"
38
+ failure_message_expecting(false)
44
39
  end
45
40
 
46
41
  # @api private
47
42
  # @return [String]
48
43
  def description
49
- [method_description, args_description].compact.join(' ')
44
+ "#{method_description}#{args_to_sentence}"
50
45
  end
51
46
 
52
47
  private
53
48
 
54
49
  def predicate_accessible?
55
- !private_predicate? && predicate_exists?
50
+ @actual.respond_to? predicate
56
51
  end
57
52
 
58
53
  # support 1.8.7, evaluate once at load time for performance
@@ -68,56 +63,105 @@ module RSpec
68
63
  end
69
64
  end
70
65
 
71
- def predicate_exists?
72
- @actual.respond_to? predicate
66
+ def predicate_result
67
+ @predicate_result = actual.__send__(predicate_method_name, *@args, &@block)
73
68
  end
74
69
 
75
- if RSpec::Support::RubyFeatures.kw_args_supported?
76
- binding.eval(<<-CODE, __FILE__, __LINE__)
77
- def predicate_matches?
78
- if @kwargs.empty?
79
- @actual.__send__(predicate, *@args, &@block)
80
- else
81
- @actual.__send__(predicate, *@args, **@kwargs, &@block)
82
- end
83
- end
84
- CODE
85
- else
86
- def predicate_matches?
87
- @actual.__send__(predicate, *@args, &@block)
70
+ def predicate_method_name
71
+ predicate
72
+ end
73
+
74
+ def predicate_matches?(value=true)
75
+ if RSpec::Expectations.configuration.strict_predicate_matchers?
76
+ value == predicate_result
77
+ else
78
+ value == !!predicate_result
88
79
  end
89
80
  end
90
81
 
91
- def predicate
82
+ def root
92
83
  # On 1.9, there appears to be a bug where String#match can return `false`
93
84
  # rather than the match data object. Changing to Regex#match appears to
94
85
  # work around this bug. For an example of this bug, see:
95
86
  # https://travis-ci.org/rspec/rspec-expectations/jobs/27549635
96
- @predicate ||= :"has_#{Matchers::HAS_REGEX.match(@method_name.to_s).captures.first}?"
87
+ self.class::REGEX.match(@method_name.to_s).captures.first
97
88
  end
98
89
 
99
90
  def method_description
100
- @method_name.to_s.tr('_', ' ')
91
+ EnglishPhrasing.split_words(@method_name)
101
92
  end
102
93
 
103
- def args_description
104
- return nil if @args.empty?
105
- @args.map { |arg| RSpec::Support::ObjectFormatter.format(arg) }.join(', ')
94
+ def failure_message_expecting(value)
95
+ validity_message ||
96
+ "expected `#{actual_formatted}.#{predicate}#{args_to_s}` to #{expectation_of value}, got #{description_of @predicate_result}"
106
97
  end
107
98
 
108
- def failure_message_args_description
109
- desc = args_description
110
- "(#{desc})" if desc
99
+ def expectation_of(value)
100
+ if RSpec::Expectations.configuration.strict_predicate_matchers?
101
+ "return #{value}"
102
+ elsif value
103
+ "be truthy"
104
+ else
105
+ "be falsey"
106
+ end
111
107
  end
112
108
 
113
109
  def validity_message
110
+ return nil if predicate_accessible?
111
+
112
+ "expected #{actual_formatted} to respond to `#{predicate}`#{failure_to_respond_explanation}"
113
+ end
114
+
115
+ def failure_to_respond_explanation
114
116
  if private_predicate?
115
- "expected #{@actual} to respond to `#{predicate}` but `#{predicate}` is a private method"
116
- elsif !predicate_exists?
117
- "expected #{@actual} to respond to `#{predicate}`"
117
+ " but `#{predicate}` is a private method"
118
118
  end
119
119
  end
120
120
  end
121
+
122
+ # @api private
123
+ # Provides the implementation for `has_<predicate>`.
124
+ # Not intended to be instantiated directly.
125
+ class Has < DynamicPredicate
126
+ # :nodoc:
127
+ REGEX = Matchers::HAS_REGEX
128
+ private
129
+ def predicate
130
+ @predicate ||= :"has_#{root}?"
131
+ end
132
+ end
133
+
134
+ # @api private
135
+ # Provides the implementation of `be_<predicate>`.
136
+ # Not intended to be instantiated directly.
137
+ class BePredicate < DynamicPredicate
138
+ # :nodoc:
139
+ REGEX = Matchers::BE_PREDICATE_REGEX
140
+ private
141
+ def predicate
142
+ @predicate ||= :"#{root}?"
143
+ end
144
+
145
+ def predicate_method_name
146
+ actual.respond_to?(predicate) ? predicate : present_tense_predicate
147
+ end
148
+
149
+ def failure_to_respond_explanation
150
+ super || if predicate == :true?
151
+ " or perhaps you meant `be true` or `be_truthy`"
152
+ elsif predicate == :false?
153
+ " or perhaps you meant `be false` or `be_falsey`"
154
+ end
155
+ end
156
+
157
+ def predicate_accessible?
158
+ super || actual.respond_to?(present_tense_predicate)
159
+ end
160
+
161
+ def present_tense_predicate
162
+ :"#{root}s?"
163
+ end
164
+ end
121
165
  end
122
166
  end
123
167
  end
@@ -1,13 +1,17 @@
1
+ require 'rspec/matchers/built_in/count_expectation'
2
+
1
3
  module RSpec
2
4
  module Matchers
3
5
  module BuiltIn
4
6
  # @api private
5
7
  # Provides the implementation for `include`.
6
8
  # Not intended to be instantiated directly.
7
- class Include < BaseMatcher
9
+ class Include < BaseMatcher # rubocop:disable Metrics/ClassLength
10
+ include CountExpectation
8
11
  # @private
9
12
  attr_reader :expecteds
10
13
 
14
+ # @api private
11
15
  def initialize(*expecteds)
12
16
  @expecteds = expecteds
13
17
  end
@@ -15,21 +19,29 @@ module RSpec
15
19
  # @api private
16
20
  # @return [Boolean]
17
21
  def matches?(actual)
18
- actual = actual.to_hash if convert_to_hash?(actual)
19
- perform_match(actual) { |v| v }
22
+ check_actual?(actual) &&
23
+ if check_expected_count?
24
+ expected_count_matches?(count_inclusions)
25
+ else
26
+ perform_match { |v| v }
27
+ end
20
28
  end
21
29
 
22
30
  # @api private
23
31
  # @return [Boolean]
24
32
  def does_not_match?(actual)
25
- actual = actual.to_hash if convert_to_hash?(actual)
26
- perform_match(actual) { |v| !v }
33
+ check_actual?(actual) &&
34
+ if check_expected_count?
35
+ !expected_count_matches?(count_inclusions)
36
+ else
37
+ perform_match { |v| !v }
38
+ end
27
39
  end
28
40
 
29
41
  # @api private
30
42
  # @return [String]
31
43
  def description
32
- improve_hash_formatting("include#{readable_list_of(expecteds)}")
44
+ improve_hash_formatting("include#{readable_list_of(expecteds)}#{count_expectation_description}")
33
45
  end
34
46
 
35
47
  # @api private
@@ -62,12 +74,33 @@ module RSpec
62
74
 
63
75
  private
64
76
 
65
- def format_failure_message(preposition)
66
- if actual.respond_to?(:include?)
67
- improve_hash_formatting("expected #{description_of @actual} #{preposition} include#{readable_list_of @divergent_items}")
68
- else
69
- improve_hash_formatting(yield) + ", but it does not respond to `include?`"
77
+ def check_actual?(actual)
78
+ actual = actual.to_hash if convert_to_hash?(actual)
79
+ @actual = actual
80
+ @actual.respond_to?(:include?)
81
+ end
82
+
83
+ def check_expected_count?
84
+ case
85
+ when !has_expected_count?
86
+ return false
87
+ when expecteds.size != 1
88
+ raise NotImplementedError, 'Count constraint supported only when testing for a single value being included'
89
+ when actual.is_a?(Hash)
90
+ raise NotImplementedError, 'Count constraint on hash keys not implemented'
70
91
  end
92
+ true
93
+ end
94
+
95
+ def format_failure_message(preposition)
96
+ msg = if actual.respond_to?(:include?)
97
+ "expected #{description_of @actual} #{preposition}" \
98
+ " include#{readable_list_of @divergent_items}" \
99
+ "#{count_failure_reason('it is included') if has_expected_count?}"
100
+ else
101
+ "#{yield}, but it does not respond to `include?`"
102
+ end
103
+ improve_hash_formatting(msg)
71
104
  end
72
105
 
73
106
  def readable_list_of(items)
@@ -79,10 +112,9 @@ module RSpec
79
112
  end
80
113
  end
81
114
 
82
- def perform_match(actual, &block)
83
- @actual = actual
115
+ def perform_match(&block)
84
116
  @divergent_items = excluded_from_actual(&block)
85
- actual.respond_to?(:include?) && @divergent_items.empty?
117
+ @divergent_items.empty?
86
118
  end
87
119
 
88
120
  def excluded_from_actual
@@ -107,7 +139,10 @@ module RSpec
107
139
  end
108
140
 
109
141
  def actual_hash_includes?(expected_key, expected_value)
110
- actual_value = actual.fetch(expected_key) { return false }
142
+ actual_value =
143
+ actual.fetch(expected_key) do
144
+ actual.find(Proc.new { return false }) { |actual_key, _| values_match?(expected_key, actual_key) }[1]
145
+ end
111
146
  values_match?(expected_value, actual_value)
112
147
  end
113
148
 
@@ -131,6 +166,28 @@ module RSpec
131
166
  actual.any? { |value| values_match?(expected_item, value) }
132
167
  end
133
168
 
169
+ if RUBY_VERSION < '1.9'
170
+ def count_enumerable(expected_item)
171
+ actual.select { |value| values_match?(expected_item, value) }.size
172
+ end
173
+ else
174
+ def count_enumerable(expected_item)
175
+ actual.count { |value| values_match?(expected_item, value) }
176
+ end
177
+ end
178
+
179
+ def count_inclusions
180
+ @divergent_items = expected
181
+ case actual
182
+ when String
183
+ actual.scan(expected.first).length
184
+ when Enumerable
185
+ count_enumerable(Hash === expected ? expected : expected.first)
186
+ else
187
+ raise NotImplementedError, 'Count constraints are implemented for Enumerable and String values only'
188
+ end
189
+ end
190
+
134
191
  def diff_would_wrongly_highlight_matched_item?
135
192
  return false unless actual.is_a?(String) && expected.is_a?(Array)
136
193
 
@@ -9,13 +9,20 @@ module RSpec
9
9
  class RaiseError
10
10
  include Composable
11
11
 
12
- def initialize(expected_error_or_message=nil, expected_message=nil, &block)
12
+ # Used as a sentinel value to be able to tell when the user did not pass an
13
+ # argument. We can't use `nil` for that because we need to warn when `nil` is
14
+ # passed in a different way. It's an Object, not a Module, since Module's `===`
15
+ # does not evaluate to true when compared to itself.
16
+ UndefinedValue = Object.new.freeze
17
+
18
+ def initialize(expected_error_or_message, expected_message, &block)
13
19
  @block = block
14
20
  @actual_error = nil
15
- @warn_about_bare_error = expected_error_or_message.nil?
21
+ @warn_about_bare_error = UndefinedValue === expected_error_or_message
22
+ @warn_about_nil_error = expected_error_or_message.nil?
16
23
 
17
24
  case expected_error_or_message
18
- when nil
25
+ when nil, UndefinedValue
19
26
  @expected_error = Exception
20
27
  @expected_message = expected_message
21
28
  when String
@@ -52,14 +59,17 @@ module RSpec
52
59
  given_proc.call
53
60
  rescue Exception => @actual_error
54
61
  if values_match?(@expected_error, @actual_error) ||
55
- values_match?(@expected_error, @actual_error.message)
62
+ values_match?(@expected_error, actual_error_message)
56
63
  @raised_expected_error = true
57
64
  @with_expected_message = verify_message
58
65
  end
59
66
  end
60
67
 
61
- warn_about_bare_error if warning_about_bare_error && !negative_expectation
62
- eval_block if !negative_expectation && ready_to_eval_block?
68
+ unless negative_expectation
69
+ warn_about_bare_error! if warn_about_bare_error?
70
+ warn_about_nil_error! if warn_about_nil_error?
71
+ eval_block if ready_to_eval_block?
72
+ end
63
73
 
64
74
  expectation_matched?
65
75
  end
@@ -67,7 +77,7 @@ module RSpec
67
77
 
68
78
  # @private
69
79
  def does_not_match?(given_proc)
70
- warn_for_false_positives
80
+ warn_for_negative_false_positives!
71
81
  !matches?(given_proc, :negative_expectation) && Proc === given_proc
72
82
  end
73
83
 
@@ -83,7 +93,7 @@ module RSpec
83
93
  # @api private
84
94
  # @return [String]
85
95
  def failure_message
86
- @eval_block ? @actual_error.message : "expected #{expected_error}#{given_error}"
96
+ @eval_block ? actual_error_message : "expected #{expected_error}#{given_error}"
87
97
  end
88
98
 
89
99
  # @api private
@@ -100,6 +110,12 @@ module RSpec
100
110
 
101
111
  private
102
112
 
113
+ def actual_error_message
114
+ return nil unless @actual_error
115
+
116
+ @actual_error.respond_to?(:original_message) ? @actual_error.original_message : @actual_error.message
117
+ end
118
+
103
119
  def expectation_matched?
104
120
  error_and_message_match? && block_matches?
105
121
  end
@@ -128,32 +144,38 @@ module RSpec
128
144
 
129
145
  def verify_message
130
146
  return true if @expected_message.nil?
131
- values_match?(@expected_message, @actual_error.message.to_s)
147
+ values_match?(@expected_message, actual_error_message.to_s)
132
148
  end
133
149
 
134
- def warn_for_false_positives
150
+ def warn_for_negative_false_positives!
135
151
  expression = if expecting_specific_exception? && @expected_message
136
152
  "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
137
153
  elsif expecting_specific_exception?
138
154
  "`expect { }.not_to raise_error(SpecificErrorClass)`"
139
155
  elsif @expected_message
140
156
  "`expect { }.not_to raise_error(message)`"
157
+ elsif @warn_about_nil_error
158
+ "`expect { }.not_to raise_error(nil)`"
141
159
  end
142
160
 
143
161
  return unless expression
144
162
 
145
- warn_about_negative_false_positive expression
163
+ warn_about_negative_false_positive! expression
146
164
  end
147
165
 
148
166
  def handle_warning(message)
149
167
  RSpec::Expectations.configuration.false_positives_handler.call(message)
150
168
  end
151
169
 
152
- def warning_about_bare_error
170
+ def warn_about_bare_error?
153
171
  @warn_about_bare_error && @block.nil?
154
172
  end
155
173
 
156
- def warn_about_bare_error
174
+ def warn_about_nil_error?
175
+ @warn_about_nil_error
176
+ end
177
+
178
+ def warn_about_bare_error!
157
179
  handle_warning("Using the `raise_error` matcher without providing a specific " \
158
180
  "error or message risks false positives, since `raise_error` " \
159
181
  "will match when Ruby raises a `NoMethodError`, `NameError` or " \
@@ -166,11 +188,24 @@ module RSpec
166
188
  "_positives = :nothing`")
167
189
  end
168
190
 
169
- def warn_about_negative_false_positive(expression)
191
+ def warn_about_nil_error!
192
+ handle_warning("Using the `raise_error` matcher with a `nil` error is probably " \
193
+ "unintentional, it risks false positives, since `raise_error` " \
194
+ "will match when Ruby raises a `NoMethodError`, `NameError` or " \
195
+ "`ArgumentError`, potentially allowing the expectation to pass " \
196
+ "without even executing the method you are intending to call. " \
197
+ "#{warning}"\
198
+ "Instead consider providing a specific error class or message. " \
199
+ "This message can be suppressed by setting: " \
200
+ "`RSpec::Expectations.configuration.on_potential_false" \
201
+ "_positives = :nothing`")
202
+ end
203
+
204
+ def warn_about_negative_false_positive!(expression)
170
205
  handle_warning("Using #{expression} risks false positives, since literally " \
171
206
  "any other error would cause the expectation to pass, " \
172
- "including those raised by Ruby (e.g. NoMethodError, NameError " \
173
- "and ArgumentError), meaning the code you are intending to test " \
207
+ "including those raised by Ruby (e.g. `NoMethodError`, `NameError` " \
208
+ "and `ArgumentError`), meaning the code you are intending to test " \
174
209
  "may not even get reached. Instead consider using " \
175
210
  "`expect { }.not_to raise_error` or `expect { }.to raise_error" \
176
211
  "(DifferentSpecificErrorClass)`. This message can be suppressed by " \
@@ -1,7 +1,5 @@
1
1
  RSpec::Support.require_rspec_support "method_signature_verifier"
2
2
 
3
- # TODO: Refactor this file to be under our class length
4
- # rubocop:disable ClassLength
5
3
  module RSpec
6
4
  module Matchers
7
5
  module BuiltIn
@@ -118,49 +116,15 @@ module RSpec
118
116
  end
119
117
  end
120
118
 
121
- def setup_method_signature_expectation
122
- expectation = Support::MethodSignatureExpectation.new
123
-
124
- if @expected_arity.is_a?(Range)
125
- expectation.min_count = @expected_arity.min
126
- expectation.max_count = @expected_arity.max
127
- else
128
- expectation.min_count = @expected_arity
129
- end
130
-
131
- expectation.keywords = @expected_keywords
132
- expectation.expect_unlimited_arguments = @unlimited_arguments
133
- expectation.expect_arbitrary_keywords = @arbitrary_keywords
134
-
135
- expectation
136
- end
137
-
138
119
  def matches_arity?(actual, name)
139
- expectation = setup_method_signature_expectation
140
-
141
- return true if expectation.empty?
142
-
143
- begin
144
- Support::StrictSignatureVerifier.new(method_signature_for(actual, name)).
145
- with_expectation(expectation).valid?
146
- rescue NameError
147
- return true if @ignoring_method_signature_failure
148
- raise ArgumentError, "The #{matcher_name} matcher requires that " \
149
- "the actual object define the method(s) in " \
150
- "order to check arity, but the method " \
151
- "`#{name}` is not defined. Remove the arity " \
152
- "check or define the method to continue."
153
- end
154
- end
155
-
156
- def method_signature_for(actual, name)
157
- method_handle = Support.method_handle_for(actual, name)
158
-
159
- if name == :new && method_handle.owner === ::Class && ::Class === actual
160
- Support::MethodSignature.new(actual.instance_method(:initialize))
161
- else
162
- Support::MethodSignature.new(method_handle)
163
- end
120
+ ArityCheck.new(@expected_arity, @expected_keywords, @arbitrary_keywords, @unlimited_arguments).matches?(actual, name)
121
+ rescue NameError
122
+ return true if @ignoring_method_signature_failure
123
+ raise ArgumentError, "The #{matcher_name} matcher requires that " \
124
+ "the actual object define the method(s) in " \
125
+ "order to check arity, but the method " \
126
+ "`#{name}` is not defined. Remove the arity " \
127
+ "check or define the method to continue."
164
128
  end
165
129
 
166
130
  def with_arity
@@ -192,8 +156,45 @@ module RSpec
192
156
  def pp_names
193
157
  @names.length == 1 ? "##{@names.first}" : description_of(@names)
194
158
  end
159
+
160
+ # @private
161
+ class ArityCheck
162
+ def initialize(expected_arity, expected_keywords, arbitrary_keywords, unlimited_arguments)
163
+ expectation = Support::MethodSignatureExpectation.new
164
+
165
+ if expected_arity.is_a?(Range)
166
+ expectation.min_count = expected_arity.min
167
+ expectation.max_count = expected_arity.max
168
+ else
169
+ expectation.min_count = expected_arity
170
+ end
171
+
172
+ expectation.keywords = expected_keywords
173
+ expectation.expect_unlimited_arguments = unlimited_arguments
174
+ expectation.expect_arbitrary_keywords = arbitrary_keywords
175
+ @expectation = expectation
176
+ end
177
+
178
+ def matches?(actual, name)
179
+ return true if @expectation.empty?
180
+ verifier_for(actual, name).with_expectation(@expectation).valid?
181
+ end
182
+
183
+ def verifier_for(actual, name)
184
+ Support::StrictSignatureVerifier.new(method_signature_for(actual, name))
185
+ end
186
+
187
+ def method_signature_for(actual, name)
188
+ method_handle = Support.method_handle_for(actual, name)
189
+
190
+ if name == :new && method_handle.owner === ::Class && ::Class === actual
191
+ Support::MethodSignature.new(actual.instance_method(:initialize))
192
+ else
193
+ Support::MethodSignature.new(method_handle)
194
+ end
195
+ end
196
+ end
195
197
  end
196
198
  end
197
199
  end
198
200
  end
199
- # rubocop:enable ClassLength