rspec-expectations 3.9.4 → 3.10.0

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