rspec-expectations 3.9.4 → 3.10.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.
@@ -0,0 +1,169 @@
1
+ module RSpec
2
+ module Matchers
3
+ module BuiltIn
4
+ # @api private
5
+ # Asbtract class to implement `once`, `at_least` and other
6
+ # count constraints.
7
+ module CountExpectation
8
+ # @api public
9
+ # Specifies that the method is expected to match once.
10
+ def once
11
+ exactly(1)
12
+ end
13
+
14
+ # @api public
15
+ # Specifies that the method is expected to match twice.
16
+ def twice
17
+ exactly(2)
18
+ end
19
+
20
+ # @api public
21
+ # Specifies that the method is expected to match thrice.
22
+ def thrice
23
+ exactly(3)
24
+ end
25
+
26
+ # @api public
27
+ # Specifies that the method is expected to match the given number of times.
28
+ def exactly(number)
29
+ set_expected_count(:==, number)
30
+ self
31
+ end
32
+
33
+ # @api public
34
+ # Specifies the maximum number of times the method is expected to match
35
+ def at_most(number)
36
+ set_expected_count(:<=, number)
37
+ self
38
+ end
39
+
40
+ # @api public
41
+ # Specifies the minimum number of times the method is expected to match
42
+ def at_least(number)
43
+ set_expected_count(:>=, number)
44
+ self
45
+ end
46
+
47
+ # @api public
48
+ # No-op. Provides syntactic sugar.
49
+ def times
50
+ self
51
+ end
52
+
53
+ protected
54
+ # @api private
55
+ attr_reader :count_expectation_type, :expected_count
56
+
57
+ private
58
+
59
+ if RUBY_VERSION.to_f > 1.8
60
+ def cover?(count, number)
61
+ count.cover?(number)
62
+ end
63
+ else
64
+ def cover?(count, number)
65
+ number >= count.first && number <= count.last
66
+ end
67
+ end
68
+
69
+ def expected_count_matches?(actual_count)
70
+ @actual_count = actual_count
71
+ return @actual_count > 0 unless count_expectation_type
72
+ return cover?(expected_count, actual_count) if count_expectation_type == :<=>
73
+
74
+ @actual_count.__send__(count_expectation_type, expected_count)
75
+ end
76
+
77
+ def has_expected_count?
78
+ !!count_expectation_type
79
+ end
80
+
81
+ def set_expected_count(relativity, n)
82
+ raise_unsupported_count_expectation if unsupported_count_expectation?(relativity)
83
+
84
+ count = count_constraint_to_number(n)
85
+
86
+ if count_expectation_type == :<= && relativity == :>=
87
+ raise_impossible_count_expectation(count) if count > expected_count
88
+ @count_expectation_type = :<=>
89
+ @expected_count = count..expected_count
90
+ elsif count_expectation_type == :>= && relativity == :<=
91
+ raise_impossible_count_expectation(count) if count < expected_count
92
+ @count_expectation_type = :<=>
93
+ @expected_count = expected_count..count
94
+ else
95
+ @count_expectation_type = relativity
96
+ @expected_count = count
97
+ end
98
+ end
99
+
100
+ def raise_impossible_count_expectation(count)
101
+ text =
102
+ case count_expectation_type
103
+ when :<= then "at_least(#{count}).at_most(#{expected_count})"
104
+ when :>= then "at_least(#{expected_count}).at_most(#{count})"
105
+ end
106
+ raise ArgumentError, "The constraint #{text} is not possible"
107
+ end
108
+
109
+ def raise_unsupported_count_expectation
110
+ text =
111
+ case count_expectation_type
112
+ when :<= then "at_least"
113
+ when :>= then "at_most"
114
+ when :<=> then "at_least/at_most combination"
115
+ else "count"
116
+ end
117
+ raise ArgumentError, "Multiple #{text} constraints are not supported"
118
+ end
119
+
120
+ def count_constraint_to_number(n)
121
+ case n
122
+ when Numeric then n
123
+ when :once then 1
124
+ when :twice then 2
125
+ when :thrice then 3
126
+ else
127
+ raise ArgumentError, "Expected a number, :once, :twice or :thrice," \
128
+ " but got #{n}"
129
+ end
130
+ end
131
+
132
+ def unsupported_count_expectation?(relativity)
133
+ return true if count_expectation_type == :==
134
+ return true if count_expectation_type == :<=>
135
+ (count_expectation_type == :<= && relativity == :<=) ||
136
+ (count_expectation_type == :>= && relativity == :>=)
137
+ end
138
+
139
+ def count_expectation_description
140
+ "#{human_readable_expectation_type}#{human_readable_count(expected_count)}"
141
+ end
142
+
143
+ def count_failure_reason(action)
144
+ "#{count_expectation_description}" \
145
+ " but #{action}#{human_readable_count(@actual_count)}"
146
+ end
147
+
148
+ def human_readable_expectation_type
149
+ case count_expectation_type
150
+ when :<= then ' at most'
151
+ when :>= then ' at least'
152
+ when :<=> then ' between'
153
+ else ''
154
+ end
155
+ end
156
+
157
+ def human_readable_count(count)
158
+ case count
159
+ when Range then " #{count.first} and #{count.last} times"
160
+ when nil then ''
161
+ when 1 then ' once'
162
+ when 2 then ' twice'
163
+ else " #{count} times"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -2,9 +2,11 @@ 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
5
+ # Provides the implementation for dynamic predicate matchers.
6
+ # Not intended to be inherited directly.
7
+ class DynamicPredicate < BaseMatcher
8
+ include BeHelpers
9
+
8
10
  def initialize(method_name, *args, &block)
9
11
  @method_name, @args, @block = method_name, args, block
10
12
  end
@@ -21,31 +23,31 @@ module RSpec
21
23
  def does_not_match?(actual, &block)
22
24
  @actual = actual
23
25
  @block ||= block
24
- predicate_accessible? && !predicate_matches?
26
+ predicate_accessible? && predicate_matches?(false)
25
27
  end
26
28
 
27
29
  # @api private
28
30
  # @return [String]
29
31
  def failure_message
30
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return true, got false"
32
+ failure_message_expecting(true)
31
33
  end
32
34
 
33
35
  # @api private
34
36
  # @return [String]
35
37
  def failure_message_when_negated
36
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return false, got true"
38
+ failure_message_expecting(false)
37
39
  end
38
40
 
39
41
  # @api private
40
42
  # @return [String]
41
43
  def description
42
- [method_description, args_description].compact.join(' ')
44
+ "#{method_description}#{args_to_sentence}"
43
45
  end
44
46
 
45
47
  private
46
48
 
47
49
  def predicate_accessible?
48
- !private_predicate? && predicate_exists?
50
+ @actual.respond_to? predicate
49
51
  end
50
52
 
51
53
  # support 1.8.7, evaluate once at load time for performance
@@ -61,44 +63,105 @@ module RSpec
61
63
  end
62
64
  end
63
65
 
64
- def predicate_exists?
65
- @actual.respond_to? predicate
66
+ def predicate_result
67
+ @predicate_result = actual.__send__(predicate_method_name, *@args, &@block)
66
68
  end
67
69
 
68
- def predicate_matches?
69
- @actual.__send__(predicate, *@args, &@block)
70
+ def predicate_method_name
71
+ predicate
70
72
  end
71
73
 
72
- def predicate
74
+ def predicate_matches?(value=true)
75
+ if RSpec::Expectations.configuration.strict_predicate_matchers?
76
+ value == predicate_result
77
+ else
78
+ value == !!predicate_result
79
+ end
80
+ end
81
+
82
+ def root
73
83
  # On 1.9, there appears to be a bug where String#match can return `false`
74
84
  # rather than the match data object. Changing to Regex#match appears to
75
85
  # work around this bug. For an example of this bug, see:
76
86
  # https://travis-ci.org/rspec/rspec-expectations/jobs/27549635
77
- @predicate ||= :"has_#{Matchers::HAS_REGEX.match(@method_name.to_s).captures.first}?"
87
+ self.class::REGEX.match(@method_name.to_s).captures.first
78
88
  end
79
89
 
80
90
  def method_description
81
- @method_name.to_s.tr('_', ' ')
91
+ EnglishPhrasing.split_words(@method_name)
82
92
  end
83
93
 
84
- def args_description
85
- return nil if @args.empty?
86
- @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}"
87
97
  end
88
98
 
89
- def failure_message_args_description
90
- desc = args_description
91
- "(#{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
92
107
  end
93
108
 
94
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
95
116
  if private_predicate?
96
- "expected #{@actual} to respond to `#{predicate}` but `#{predicate}` is a private method"
97
- elsif !predicate_exists?
98
- "expected #{@actual} to respond to `#{predicate}`"
117
+ " but `#{predicate}` is a private method"
99
118
  end
100
119
  end
101
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
102
165
  end
103
166
  end
104
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(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