rspec-expectations 3.9.3 → 3.10.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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