rspec-expectations 3.0.4 → 3.12.3

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