rspec-expectations 3.0.4 → 3.12.3

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.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.document +1 -1
  4. data/.yardopts +1 -1
  5. data/Changelog.md +530 -5
  6. data/{License.txt → LICENSE.md} +5 -4
  7. data/README.md +73 -31
  8. data/lib/rspec/expectations/block_snippet_extractor.rb +253 -0
  9. data/lib/rspec/expectations/configuration.rb +96 -1
  10. data/lib/rspec/expectations/expectation_target.rb +82 -38
  11. data/lib/rspec/expectations/fail_with.rb +11 -6
  12. data/lib/rspec/expectations/failure_aggregator.rb +229 -0
  13. data/lib/rspec/expectations/handler.rb +36 -15
  14. data/lib/rspec/expectations/minitest_integration.rb +43 -2
  15. data/lib/rspec/expectations/syntax.rb +5 -5
  16. data/lib/rspec/expectations/version.rb +1 -1
  17. data/lib/rspec/expectations.rb +15 -1
  18. data/lib/rspec/matchers/aliased_matcher.rb +79 -4
  19. data/lib/rspec/matchers/built_in/all.rb +11 -0
  20. data/lib/rspec/matchers/built_in/base_matcher.rb +111 -28
  21. data/lib/rspec/matchers/built_in/be.rb +28 -114
  22. data/lib/rspec/matchers/built_in/be_between.rb +1 -1
  23. data/lib/rspec/matchers/built_in/be_instance_of.rb +5 -1
  24. data/lib/rspec/matchers/built_in/be_kind_of.rb +5 -1
  25. data/lib/rspec/matchers/built_in/be_within.rb +5 -12
  26. data/lib/rspec/matchers/built_in/change.rb +171 -63
  27. data/lib/rspec/matchers/built_in/compound.rb +201 -30
  28. data/lib/rspec/matchers/built_in/contain_exactly.rb +73 -12
  29. data/lib/rspec/matchers/built_in/count_expectation.rb +169 -0
  30. data/lib/rspec/matchers/built_in/eq.rb +3 -38
  31. data/lib/rspec/matchers/built_in/eql.rb +2 -2
  32. data/lib/rspec/matchers/built_in/equal.rb +3 -3
  33. data/lib/rspec/matchers/built_in/exist.rb +7 -3
  34. data/lib/rspec/matchers/built_in/has.rb +93 -30
  35. data/lib/rspec/matchers/built_in/have_attributes.rb +114 -0
  36. data/lib/rspec/matchers/built_in/include.rb +133 -25
  37. data/lib/rspec/matchers/built_in/match.rb +79 -2
  38. data/lib/rspec/matchers/built_in/operators.rb +14 -5
  39. data/lib/rspec/matchers/built_in/output.rb +59 -2
  40. data/lib/rspec/matchers/built_in/raise_error.rb +130 -27
  41. data/lib/rspec/matchers/built_in/respond_to.rb +117 -15
  42. data/lib/rspec/matchers/built_in/satisfy.rb +28 -14
  43. data/lib/rspec/matchers/built_in/{start_and_end_with.rb → start_or_end_with.rb} +20 -8
  44. data/lib/rspec/matchers/built_in/throw_symbol.rb +15 -5
  45. data/lib/rspec/matchers/built_in/yield.rb +129 -156
  46. data/lib/rspec/matchers/built_in.rb +5 -3
  47. data/lib/rspec/matchers/composable.rb +24 -36
  48. data/lib/rspec/matchers/dsl.rb +203 -37
  49. data/lib/rspec/matchers/english_phrasing.rb +58 -0
  50. data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +82 -0
  51. data/lib/rspec/matchers/fail_matchers.rb +42 -0
  52. data/lib/rspec/matchers/generated_descriptions.rb +1 -2
  53. data/lib/rspec/matchers/matcher_delegator.rb +3 -4
  54. data/lib/rspec/matchers/matcher_protocol.rb +105 -0
  55. data/lib/rspec/matchers.rb +267 -144
  56. data.tar.gz.sig +0 -0
  57. metadata +71 -49
  58. metadata.gz.sig +0 -0
  59. data/lib/rspec/matchers/pretty.rb +0 -77
@@ -48,14 +48,14 @@ MESSAGE
48
48
 
49
49
  def actual_inspected
50
50
  if LITERAL_SINGLETONS.include?(actual)
51
- actual.inspect
51
+ actual_formatted
52
52
  else
53
53
  inspect_object(actual)
54
54
  end
55
55
  end
56
56
 
57
57
  def simple_failure_message
58
- "\nexpected #{expected.inspect}\n got #{actual_inspected}\n"
58
+ "\nexpected #{expected_formatted}\n got #{actual_inspected}\n"
59
59
  end
60
60
 
61
61
  def detailed_failure_message
@@ -73,7 +73,7 @@ MESSAGE
73
73
  end
74
74
 
75
75
  def inspect_object(o)
76
- "#<#{o.class}:#{o.object_id}> => #{o.inspect}"
76
+ "#<#{o.class}:#{o.object_id}> => #{RSpec::Support::ObjectFormatter.format(o)}"
77
77
  end
78
78
  end
79
79
  end
@@ -28,13 +28,13 @@ module RSpec
28
28
  # @api private
29
29
  # @return [String]
30
30
  def failure_message
31
- "expected #{@actual.inspect} to exist#{@test.validity_message}"
31
+ "expected #{actual_formatted} to exist#{@test.validity_message}"
32
32
  end
33
33
 
34
34
  # @api private
35
35
  # @return [String]
36
36
  def failure_message_when_negated
37
- "expected #{@actual.inspect} not to exist#{@test.validity_message}"
37
+ "expected #{actual_formatted} not to exist#{@test.validity_message}"
38
38
  end
39
39
 
40
40
  # @api private
@@ -77,7 +77,11 @@ module RSpec
77
77
  end
78
78
 
79
79
  def predicates
80
- @predicates ||= [:exist?, :exists?].select { |p| actual.respond_to?(p) }
80
+ @predicates ||= [:exist?, :exists?].select { |p| actual.respond_to?(p) && !deprecated(p, actual) }
81
+ end
82
+
83
+ def deprecated(predicate, actual)
84
+ predicate == :exists? && (File == actual || FileTest == actual || Dir == actual)
81
85
  end
82
86
  end
83
87
  end
@@ -2,14 +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
8
- include Composable
5
+ # Provides the implementation for dynamic predicate matchers.
6
+ # Not intended to be inherited directly.
7
+ class DynamicPredicate < BaseMatcher
8
+ include BeHelpers
9
9
 
10
10
  def initialize(method_name, *args, &block)
11
11
  @method_name, @args, @block = method_name, args, block
12
12
  end
13
+ ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
13
14
 
14
15
  # @private
15
16
  def matches?(actual, &block)
@@ -22,83 +23,145 @@ module RSpec
22
23
  def does_not_match?(actual, &block)
23
24
  @actual = actual
24
25
  @block ||= block
25
- predicate_accessible? && !predicate_matches?
26
+ predicate_accessible? && predicate_matches?(false)
26
27
  end
27
28
 
28
29
  # @api private
29
30
  # @return [String]
30
31
  def failure_message
31
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return true, got false"
32
+ failure_message_expecting(true)
32
33
  end
33
34
 
34
35
  # @api private
35
36
  # @return [String]
36
37
  def failure_message_when_negated
37
- validity_message || "expected ##{predicate}#{failure_message_args_description} to return false, got true"
38
+ failure_message_expecting(false)
38
39
  end
39
40
 
40
41
  # @api private
41
42
  # @return [String]
42
43
  def description
43
- [method_description, args_description].compact.join(' ')
44
- end
45
-
46
- # @private
47
- def supports_block_expectations?
48
- false
44
+ "#{method_description}#{args_to_sentence}"
49
45
  end
50
46
 
51
47
  private
52
48
 
53
49
  def predicate_accessible?
54
- !private_predicate? && predicate_exists?
50
+ @actual.respond_to? predicate
55
51
  end
56
52
 
57
53
  # support 1.8.7, evaluate once at load time for performance
58
54
  if String === methods.first
55
+ # :nocov:
59
56
  def private_predicate?
60
57
  @actual.private_methods.include? predicate.to_s
61
58
  end
59
+ # :nocov:
62
60
  else
63
61
  def private_predicate?
64
62
  @actual.private_methods.include? predicate
65
63
  end
66
64
  end
67
65
 
68
- def predicate_exists?
69
- @actual.respond_to? predicate
66
+ def predicate_result
67
+ @predicate_result = actual.__send__(predicate_method_name, *@args, &@block)
70
68
  end
71
69
 
72
- def predicate_matches?
73
- @actual.__send__(predicate, *@args, &@block)
70
+ def predicate_method_name
71
+ predicate
74
72
  end
75
73
 
76
- def predicate
77
- @predicate ||= :"has_#{@method_name.to_s.match(Matchers::HAS_REGEX).captures.first}?"
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
83
+ # On 1.9, there appears to be a bug where String#match can return `false`
84
+ # rather than the match data object. Changing to Regex#match appears to
85
+ # work around this bug. For an example of this bug, see:
86
+ # https://travis-ci.org/rspec/rspec-expectations/jobs/27549635
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.gsub('_', ' ')
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| arg.inspect }.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
@@ -0,0 +1,114 @@
1
+ module RSpec
2
+ module Matchers
3
+ module BuiltIn
4
+ # @api private
5
+ # Provides the implementation for `have_attributes`.
6
+ # Not intended to be instantiated directly.
7
+ class HaveAttributes < BaseMatcher
8
+ # @private
9
+ attr_reader :respond_to_failed
10
+
11
+ def initialize(expected)
12
+ @expected = expected
13
+ @values = {}
14
+ @respond_to_failed = false
15
+ @negated = false
16
+ end
17
+
18
+ # @private
19
+ def actual
20
+ @values
21
+ end
22
+
23
+ # @api private
24
+ # @return [Boolean]
25
+ def matches?(actual)
26
+ @actual = actual
27
+ @negated = false
28
+ return false unless respond_to_attributes?
29
+ perform_match(:all?)
30
+ end
31
+
32
+ # @api private
33
+ # @return [Boolean]
34
+ def does_not_match?(actual)
35
+ @actual = actual
36
+ @negated = true
37
+ return false unless respond_to_attributes?
38
+ perform_match(:none?)
39
+ end
40
+
41
+ # @api private
42
+ # @return [String]
43
+ def description
44
+ described_items = surface_descriptions_in(expected)
45
+ improve_hash_formatting "have attributes #{RSpec::Support::ObjectFormatter.format(described_items)}"
46
+ end
47
+
48
+ # @api private
49
+ # @return [Boolean]
50
+ def diffable?
51
+ !@respond_to_failed && !@negated
52
+ end
53
+
54
+ # @api private
55
+ # @return [String]
56
+ def failure_message
57
+ respond_to_failure_message_or do
58
+ "expected #{actual_formatted} to #{description} but had attributes #{ formatted_values }"
59
+ end
60
+ end
61
+
62
+ # @api private
63
+ # @return [String]
64
+ def failure_message_when_negated
65
+ respond_to_failure_message_or { "expected #{actual_formatted} not to #{description}" }
66
+ end
67
+
68
+ private
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
+
78
+ def perform_match(predicate)
79
+ cache_all_values
80
+ expected.__send__(predicate) do |attribute_key, attribute_value|
81
+ actual_has_attribute?(attribute_key, attribute_value)
82
+ end
83
+ end
84
+
85
+ def actual_has_attribute?(attribute_key, attribute_value)
86
+ values_match?(attribute_value, @values.fetch(attribute_key))
87
+ end
88
+
89
+ def respond_to_attributes?
90
+ matches = respond_to_matcher.matches?(@actual)
91
+ @respond_to_failed = !matches
92
+ matches
93
+ end
94
+
95
+ def respond_to_matcher
96
+ @respond_to_matcher ||= RespondTo.new(*expected.keys).with(0).arguments.tap { |m| m.ignoring_method_signature_failure! }
97
+ end
98
+
99
+ def respond_to_failure_message_or
100
+ if respond_to_failed
101
+ respond_to_matcher.failure_message
102
+ else
103
+ improve_hash_formatting(yield)
104
+ end
105
+ end
106
+
107
+ def formatted_values
108
+ values = RSpec::Support::ObjectFormatter.format(@values)
109
+ improve_hash_formatting(values)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,73 +1,136 @@
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
8
- def initialize(*expected)
9
- @expected = expected
9
+ class Include < BaseMatcher # rubocop:disable Metrics/ClassLength
10
+ include CountExpectation
11
+ # @private
12
+ attr_reader :expecteds
13
+
14
+ # @api private
15
+ def initialize(*expecteds)
16
+ @expecteds = expecteds
10
17
  end
11
18
 
12
19
  # @api private
13
20
  # @return [Boolean]
14
21
  def matches?(actual)
15
- @actual = actual
16
- perform_match(:all?, :all?)
22
+ check_actual?(actual) &&
23
+ if check_expected_count?
24
+ expected_count_matches?(count_inclusions)
25
+ else
26
+ perform_match { |v| v }
27
+ end
17
28
  end
18
29
 
19
30
  # @api private
20
31
  # @return [Boolean]
21
32
  def does_not_match?(actual)
22
- @actual = actual
23
- perform_match(:none?, :any?)
33
+ check_actual?(actual) &&
34
+ if check_expected_count?
35
+ !expected_count_matches?(count_inclusions)
36
+ else
37
+ perform_match { |v| !v }
38
+ end
24
39
  end
25
40
 
26
41
  # @api private
27
42
  # @return [String]
28
43
  def description
29
- described_items = surface_descriptions_in(expected)
30
- improve_hash_formatting "include#{to_sentence(described_items)}"
44
+ improve_hash_formatting("include#{readable_list_of(expecteds)}#{count_expectation_description}")
31
45
  end
32
46
 
33
47
  # @api private
34
48
  # @return [String]
35
49
  def failure_message
36
- improve_hash_formatting(super) + invalid_type_message
50
+ format_failure_message("to") { super }
37
51
  end
38
52
 
39
53
  # @api private
40
54
  # @return [String]
41
55
  def failure_message_when_negated
42
- improve_hash_formatting(super) + invalid_type_message
56
+ format_failure_message("not to") { super }
43
57
  end
44
58
 
45
59
  # @api private
46
60
  # @return [Boolean]
47
61
  def diffable?
48
- true
62
+ !diff_would_wrongly_highlight_matched_item?
63
+ end
64
+
65
+ # @api private
66
+ # @return [Array, Hash]
67
+ def expected
68
+ if expecteds.one? && Hash === expecteds.first
69
+ expecteds.first
70
+ else
71
+ expecteds
72
+ end
49
73
  end
50
74
 
51
75
  private
52
76
 
53
- def invalid_type_message
54
- return '' if actual.respond_to?(:include?)
55
- ", 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?)
56
81
  end
57
82
 
58
- def perform_match(predicate, hash_subset_predicate)
59
- return false unless actual.respond_to?(:include?)
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'
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)
104
+ end
60
105
 
61
- expected.__send__(predicate) do |expected_item|
106
+ def readable_list_of(items)
107
+ described_items = surface_descriptions_in(items)
108
+ if described_items.all? { |item| item.is_a?(Hash) }
109
+ " #{described_items.inject(:merge).inspect}"
110
+ else
111
+ EnglishPhrasing.list(described_items)
112
+ end
113
+ end
114
+
115
+ def perform_match(&block)
116
+ @divergent_items = excluded_from_actual(&block)
117
+ @divergent_items.empty?
118
+ end
119
+
120
+ def excluded_from_actual
121
+ return [] unless @actual.respond_to?(:include?)
122
+
123
+ expecteds.inject([]) do |memo, expected_item|
62
124
  if comparing_hash_to_a_subset?(expected_item)
63
- expected_item.__send__(hash_subset_predicate) do |(key, value)|
64
- actual_hash_includes?(key, value)
125
+ expected_item.each do |(key, value)|
126
+ memo << { key => value } unless yield actual_hash_includes?(key, value)
65
127
  end
66
128
  elsif comparing_hash_keys?(expected_item)
67
- actual_hash_has_key?(expected_item)
129
+ memo << expected_item unless yield actual_hash_has_key?(expected_item)
68
130
  else
69
- actual_collection_includes?(expected_item)
131
+ memo << expected_item unless yield actual_collection_includes?(expected_item)
70
132
  end
133
+ memo
71
134
  end
72
135
  end
73
136
 
@@ -76,7 +139,10 @@ module RSpec
76
139
  end
77
140
 
78
141
  def actual_hash_includes?(expected_key, expected_value)
79
- 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
80
146
  values_match?(expected_value, actual_value)
81
147
  end
82
148
 
@@ -87,8 +153,15 @@ module RSpec
87
153
  def actual_hash_has_key?(expected_key)
88
154
  # We check `key?` first for perf:
89
155
  # `key?` is O(1), but `any?` is O(N).
90
- actual.key?(expected_key) ||
91
- actual.keys.any? { |key| values_match?(expected_key, key) }
156
+
157
+ has_exact_key =
158
+ begin
159
+ actual.key?(expected_key)
160
+ rescue
161
+ false
162
+ end
163
+
164
+ has_exact_key || actual.keys.any? { |key| values_match?(expected_key, key) }
92
165
  end
93
166
 
94
167
  def actual_collection_includes?(expected_item)
@@ -99,6 +172,41 @@ module RSpec
99
172
 
100
173
  actual.any? { |value| values_match?(expected_item, value) }
101
174
  end
175
+
176
+ if RUBY_VERSION < '1.9'
177
+ def count_enumerable(expected_item)
178
+ actual.select { |value| values_match?(expected_item, value) }.size
179
+ end
180
+ else
181
+ def count_enumerable(expected_item)
182
+ actual.count { |value| values_match?(expected_item, value) }
183
+ end
184
+ end
185
+
186
+ def count_inclusions
187
+ @divergent_items = expected
188
+ case actual
189
+ when String
190
+ actual.scan(expected.first).length
191
+ when Enumerable
192
+ count_enumerable(Hash === expected ? expected : expected.first)
193
+ else
194
+ raise NotImplementedError, 'Count constraints are implemented for Enumerable and String values only'
195
+ end
196
+ end
197
+
198
+ def diff_would_wrongly_highlight_matched_item?
199
+ return false unless actual.is_a?(String) && expected.is_a?(Array)
200
+
201
+ lines = actual.split("\n")
202
+ expected.any? do |str|
203
+ actual.include?(str) && lines.none? { |line| line == str }
204
+ end
205
+ end
206
+
207
+ def convert_to_hash?(obj)
208
+ !obj.respond_to?(:include?) && obj.respond_to?(:to_hash)
209
+ end
102
210
  end
103
211
  end
104
212
  end
@@ -5,10 +5,19 @@ module RSpec
5
5
  # Provides the implementation for `match`.
6
6
  # Not intended to be instantiated directly.
7
7
  class Match < BaseMatcher
8
+ def initialize(expected)
9
+ super(expected)
10
+
11
+ @expected_captures = nil
12
+ end
8
13
  # @api private
9
14
  # @return [String]
10
15
  def description
11
- "match #{surface_descriptions_in(expected).inspect}"
16
+ if @expected_captures && @expected.match(actual)
17
+ "match #{surface_descriptions_in(expected).inspect} with captures #{surface_descriptions_in(@expected_captures).inspect}"
18
+ else
19
+ "match #{surface_descriptions_in(expected).inspect}"
20
+ end
12
21
  end
13
22
 
14
23
  # @api private
@@ -17,12 +26,80 @@ module RSpec
17
26
  true
18
27
  end
19
28
 
29
+ # Used to specify the captures we match against
30
+ # @return [self]
31
+ def with_captures(*captures)
32
+ @expected_captures = captures
33
+ self
34
+ end
35
+
20
36
  private
21
37
 
22
38
  def match(expected, actual)
39
+ return match_captures(expected, actual) if @expected_captures
23
40
  return true if values_match?(expected, actual)
24
- actual.match(expected) if actual.respond_to?(:match)
41
+ return false unless can_safely_call_match?(expected, actual)
42
+ actual.match(expected)
43
+ end
44
+
45
+ def can_safely_call_match?(expected, actual)
46
+ return false unless actual.respond_to?(:match)
47
+
48
+ !(RSpec::Matchers.is_a_matcher?(expected) &&
49
+ (String === actual || Regexp === actual))
50
+ end
51
+
52
+ def match_captures(expected, actual)
53
+ match = actual.match(expected)
54
+ if match
55
+ match = ReliableMatchData.new(match)
56
+ if match.names.empty?
57
+ values_match?(@expected_captures, match.captures)
58
+ else
59
+ expected_matcher = @expected_captures.last
60
+ values_match?(expected_matcher, Hash[match.names.zip(match.captures)]) ||
61
+ values_match?(expected_matcher, Hash[match.names.map(&:to_sym).zip(match.captures)]) ||
62
+ values_match?(@expected_captures, match.captures)
63
+ end
64
+ else
65
+ false
66
+ end
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ # Used to wrap match data and make it reliable for 1.8.7
72
+ class ReliableMatchData
73
+ def initialize(match_data)
74
+ @match_data = match_data
75
+ end
76
+
77
+ if RUBY_VERSION == "1.8.7"
78
+ # @api private
79
+ # Returns match data names for named captures
80
+ # @return Array
81
+ def names
82
+ []
83
+ end
84
+ else
85
+ # @api private
86
+ # Returns match data names for named captures
87
+ # @return Array
88
+ def names
89
+ match_data.names
90
+ end
25
91
  end
92
+
93
+ # @api private
94
+ # returns an array of captures from the match data
95
+ # @return Array
96
+ def captures
97
+ match_data.captures
98
+ end
99
+
100
+ protected
101
+
102
+ attr_reader :match_data
26
103
  end
27
104
  end
28
105
  end