rspec-expectations 3.2.1 → 3.3.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/Changelog.md +55 -4
- data/README.md +1 -1
- data/lib/rspec/expectations.rb +13 -1
- data/lib/rspec/expectations/configuration.rb +17 -0
- data/lib/rspec/expectations/expectation_target.rb +3 -9
- data/lib/rspec/expectations/fail_with.rb +1 -3
- data/lib/rspec/expectations/failure_aggregator.rb +194 -0
- data/lib/rspec/expectations/minitest_integration.rb +13 -0
- data/lib/rspec/expectations/version.rb +1 -1
- data/lib/rspec/matchers.rb +59 -5
- data/lib/rspec/matchers/built_in/base_matcher.rb +56 -7
- data/lib/rspec/matchers/built_in/be.rb +25 -15
- data/lib/rspec/matchers/built_in/be_between.rb +1 -1
- data/lib/rspec/matchers/built_in/be_within.rb +2 -2
- data/lib/rspec/matchers/built_in/contain_exactly.rb +12 -8
- data/lib/rspec/matchers/built_in/eq.rb +3 -38
- data/lib/rspec/matchers/built_in/eql.rb +2 -2
- data/lib/rspec/matchers/built_in/equal.rb +3 -3
- data/lib/rspec/matchers/built_in/exist.rb +2 -2
- data/lib/rspec/matchers/built_in/has.rb +3 -1
- data/lib/rspec/matchers/built_in/have_attributes.rb +5 -4
- data/lib/rspec/matchers/built_in/include.rb +44 -19
- data/lib/rspec/matchers/built_in/match.rb +9 -1
- data/lib/rspec/matchers/built_in/operators.rb +14 -5
- data/lib/rspec/matchers/built_in/output.rb +9 -2
- data/lib/rspec/matchers/built_in/raise_error.rb +64 -22
- data/lib/rspec/matchers/built_in/respond_to.rb +2 -3
- data/lib/rspec/matchers/built_in/satisfy.rb +7 -9
- data/lib/rspec/matchers/built_in/start_or_end_with.rb +3 -1
- data/lib/rspec/matchers/built_in/throw_symbol.rb +1 -1
- data/lib/rspec/matchers/built_in/yield.rb +7 -5
- data/lib/rspec/matchers/composable.rb +5 -4
- data/lib/rspec/matchers/dsl.rb +19 -6
- data/lib/rspec/matchers/english_phrasing.rb +42 -0
- data/lib/rspec/matchers/expecteds_for_multiple_diffs.rb +2 -8
- data/lib/rspec/matchers/fail_matchers.rb +42 -0
- data/lib/rspec/matchers/matcher_delegator.rb +2 -0
- metadata +9 -7
- metadata.gz.sig +0 -0
- data/lib/rspec/matchers/pretty.rb +0 -77
@@ -42,7 +42,7 @@ module RSpec
|
|
42
42
|
# @return [String]
|
43
43
|
def description
|
44
44
|
described_items = surface_descriptions_in(expected)
|
45
|
-
improve_hash_formatting "have attributes #{described_items
|
45
|
+
improve_hash_formatting "have attributes #{RSpec::Support::ObjectFormatter.format(described_items)}"
|
46
46
|
end
|
47
47
|
|
48
48
|
# @api private
|
@@ -55,14 +55,14 @@ module RSpec
|
|
55
55
|
# @return [String]
|
56
56
|
def failure_message
|
57
57
|
respond_to_failure_message_or do
|
58
|
-
"expected #{
|
58
|
+
"expected #{actual_formatted} to #{description} but had attributes #{ formatted_values }"
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
62
|
# @api private
|
63
63
|
# @return [String]
|
64
64
|
def failure_message_when_negated
|
65
|
-
respond_to_failure_message_or { "expected #{
|
65
|
+
respond_to_failure_message_or { "expected #{actual_formatted} not to #{description}" }
|
66
66
|
end
|
67
67
|
|
68
68
|
private
|
@@ -105,7 +105,8 @@ module RSpec
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def formatted_values
|
108
|
-
|
108
|
+
values = RSpec::Support::ObjectFormatter.format(@values)
|
109
|
+
improve_hash_formatting(values)
|
109
110
|
end
|
110
111
|
end
|
111
112
|
end
|
@@ -12,62 +12,78 @@ module RSpec
|
|
12
12
|
# @api private
|
13
13
|
# @return [Boolean]
|
14
14
|
def matches?(actual)
|
15
|
-
|
16
|
-
perform_match(:all?, :all?)
|
15
|
+
perform_match(actual) { |v| v }
|
17
16
|
end
|
18
17
|
|
19
18
|
# @api private
|
20
19
|
# @return [Boolean]
|
21
20
|
def does_not_match?(actual)
|
22
|
-
|
23
|
-
perform_match(:none?, :any?)
|
21
|
+
perform_match(actual) { |v| !v }
|
24
22
|
end
|
25
23
|
|
26
24
|
# @api private
|
27
25
|
# @return [String]
|
28
26
|
def description
|
29
|
-
|
30
|
-
improve_hash_formatting "include#{to_sentence(described_items)}"
|
27
|
+
improve_hash_formatting("include#{readable_list_of(expected)}")
|
31
28
|
end
|
32
29
|
|
33
30
|
# @api private
|
34
31
|
# @return [String]
|
35
32
|
def failure_message
|
36
|
-
|
33
|
+
format_failure_message("to") { super }
|
37
34
|
end
|
38
35
|
|
39
36
|
# @api private
|
40
37
|
# @return [String]
|
41
38
|
def failure_message_when_negated
|
42
|
-
|
39
|
+
format_failure_message("not to") { super }
|
43
40
|
end
|
44
41
|
|
45
42
|
# @api private
|
46
43
|
# @return [Boolean]
|
47
44
|
def diffable?
|
48
|
-
|
45
|
+
!diff_would_wrongly_highlight_matched_item?
|
49
46
|
end
|
50
47
|
|
51
48
|
private
|
52
49
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
50
|
+
def format_failure_message(preposition)
|
51
|
+
if actual.respond_to?(:include?)
|
52
|
+
improve_hash_formatting("expected #{description_of @actual} #{preposition} include#{readable_list_of @divergent_items}")
|
53
|
+
else
|
54
|
+
improve_hash_formatting(yield) + ", but it does not respond to `include?`"
|
55
|
+
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def
|
59
|
-
|
58
|
+
def readable_list_of(items)
|
59
|
+
described_items = surface_descriptions_in(items)
|
60
|
+
if described_items.all? { |item| item.is_a?(Hash) }
|
61
|
+
" #{described_items.inject(:merge).inspect}"
|
62
|
+
else
|
63
|
+
EnglishPhrasing.list(described_items)
|
64
|
+
end
|
65
|
+
end
|
60
66
|
|
61
|
-
|
67
|
+
def perform_match(actual, &block)
|
68
|
+
@actual = actual
|
69
|
+
@divergent_items = excluded_from_actual(&block)
|
70
|
+
actual.respond_to?(:include?) && @divergent_items.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
def excluded_from_actual
|
74
|
+
return [] unless @actual.respond_to?(:include?)
|
75
|
+
|
76
|
+
expected.inject([]) do |memo, expected_item|
|
62
77
|
if comparing_hash_to_a_subset?(expected_item)
|
63
|
-
expected_item.
|
64
|
-
actual_hash_includes?(key, value)
|
78
|
+
expected_item.each do |(key, value)|
|
79
|
+
memo << { key => value } unless yield actual_hash_includes?(key, value)
|
65
80
|
end
|
66
81
|
elsif comparing_hash_keys?(expected_item)
|
67
|
-
actual_hash_has_key?(expected_item)
|
82
|
+
memo << expected_item unless yield actual_hash_has_key?(expected_item)
|
68
83
|
else
|
69
|
-
actual_collection_includes?(expected_item)
|
84
|
+
memo << expected_item unless yield actual_collection_includes?(expected_item)
|
70
85
|
end
|
86
|
+
memo
|
71
87
|
end
|
72
88
|
end
|
73
89
|
|
@@ -99,6 +115,15 @@ module RSpec
|
|
99
115
|
|
100
116
|
actual.any? { |value| values_match?(expected_item, value) }
|
101
117
|
end
|
118
|
+
|
119
|
+
def diff_would_wrongly_highlight_matched_item?
|
120
|
+
return false unless actual.is_a?(String) && expected.is_a?(Array)
|
121
|
+
|
122
|
+
lines = actual.split("\n")
|
123
|
+
expected.any? do |str|
|
124
|
+
actual.include?(str) && lines.none? { |line| line == str }
|
125
|
+
end
|
126
|
+
end
|
102
127
|
end
|
103
128
|
end
|
104
129
|
end
|
@@ -21,7 +21,15 @@ module RSpec
|
|
21
21
|
|
22
22
|
def match(expected, actual)
|
23
23
|
return true if values_match?(expected, actual)
|
24
|
-
|
24
|
+
return false unless can_safely_call_match?(expected, actual)
|
25
|
+
actual.match(expected)
|
26
|
+
end
|
27
|
+
|
28
|
+
def can_safely_call_match?(expected, actual)
|
29
|
+
return false unless actual.respond_to?(:match)
|
30
|
+
|
31
|
+
!(RSpec::Matchers.is_a_matcher?(expected) &&
|
32
|
+
(String === actual || Regexp === actual))
|
25
33
|
end
|
26
34
|
end
|
27
35
|
end
|
@@ -74,7 +74,7 @@ module RSpec
|
|
74
74
|
# @api private
|
75
75
|
# @return [String]
|
76
76
|
def description
|
77
|
-
"#{@operator} #{@expected
|
77
|
+
"#{@operator} #{RSpec::Support::ObjectFormatter.format(@expected)}"
|
78
78
|
end
|
79
79
|
|
80
80
|
private
|
@@ -98,10 +98,15 @@ module RSpec
|
|
98
98
|
def __delegate_operator(actual, operator, expected)
|
99
99
|
if actual.__send__(operator, expected)
|
100
100
|
true
|
101
|
-
elsif ['==', '===', '=~'].include?(operator)
|
102
|
-
fail_with_message("expected: #{expected.inspect}\n got: #{actual.inspect} (using #{operator})")
|
103
101
|
else
|
104
|
-
|
102
|
+
expected_formatted = RSpec::Support::ObjectFormatter.format(expected)
|
103
|
+
actual_formatted = RSpec::Support::ObjectFormatter.format(actual)
|
104
|
+
|
105
|
+
if ['==', '===', '=~'].include?(operator)
|
106
|
+
fail_with_message("expected: #{expected_formatted}\n got: #{actual_formatted} (using #{operator})")
|
107
|
+
else
|
108
|
+
fail_with_message("expected: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}")
|
109
|
+
end
|
105
110
|
end
|
106
111
|
end
|
107
112
|
end
|
@@ -111,7 +116,11 @@ module RSpec
|
|
111
116
|
class NegativeOperatorMatcher < OperatorMatcher
|
112
117
|
def __delegate_operator(actual, operator, expected)
|
113
118
|
return false unless actual.__send__(operator, expected)
|
114
|
-
|
119
|
+
|
120
|
+
expected_formatted = RSpec::Support::ObjectFormatter.format(expected)
|
121
|
+
actual_formatted = RSpec::Support::ObjectFormatter.format(actual)
|
122
|
+
|
123
|
+
fail_with_message("expected not: #{operator} #{expected_formatted}\n got: #{operator.gsub(/./, ' ')} #{actual_formatted}")
|
115
124
|
end
|
116
125
|
end
|
117
126
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'stringio'
|
2
|
-
require 'tempfile'
|
3
2
|
|
4
3
|
module RSpec
|
5
4
|
module Matchers
|
@@ -114,7 +113,7 @@ module RSpec
|
|
114
113
|
|
115
114
|
def actual_output_description
|
116
115
|
return "nothing" unless captured?
|
117
|
-
|
116
|
+
actual_formatted
|
118
117
|
end
|
119
118
|
end
|
120
119
|
|
@@ -172,6 +171,14 @@ module RSpec
|
|
172
171
|
# @private
|
173
172
|
class CaptureStreamToTempfile < Struct.new(:name, :stream)
|
174
173
|
def capture(block)
|
174
|
+
# We delay loading tempfile until it is actually needed because
|
175
|
+
# we want to minimize stdlibs loaded so that users who use a
|
176
|
+
# portion of the stdlib can't have passing specs while forgetting
|
177
|
+
# to load it themselves. `CaptureStreamToTempfile` is rarely used
|
178
|
+
# and `tempfile` pulls in a bunch of things (delegate, tmpdir,
|
179
|
+
# thread, fileutils, etc), so it's worth delaying it until this point.
|
180
|
+
require 'tempfile'
|
181
|
+
|
175
182
|
original_stream = stream.clone
|
176
183
|
captured_stream = Tempfile.new(name)
|
177
184
|
|
@@ -8,11 +8,15 @@ module RSpec
|
|
8
8
|
class RaiseError
|
9
9
|
include Composable
|
10
10
|
|
11
|
-
def initialize(expected_error_or_message=
|
11
|
+
def initialize(expected_error_or_message=nil, expected_message=nil, &block)
|
12
12
|
@block = block
|
13
13
|
@actual_error = nil
|
14
|
+
@warn_about_bare_error = warn_about_potential_false_positives? && expected_error_or_message.nil?
|
15
|
+
|
14
16
|
case expected_error_or_message
|
15
|
-
when
|
17
|
+
when nil
|
18
|
+
@expected_error, @expected_message = Exception, expected_message
|
19
|
+
when String
|
16
20
|
@expected_error, @expected_message = Exception, expected_error_or_message
|
17
21
|
else
|
18
22
|
@expected_error, @expected_message = expected_error_or_message, expected_message
|
@@ -23,6 +27,7 @@ module RSpec
|
|
23
27
|
# Specifies the expected error message.
|
24
28
|
def with_message(expected_message)
|
25
29
|
raise_message_already_set if @expected_message
|
30
|
+
@warn_about_bare_error = false
|
26
31
|
@expected_message = expected_message
|
27
32
|
self
|
28
33
|
end
|
@@ -37,20 +42,20 @@ module RSpec
|
|
37
42
|
@eval_block = false
|
38
43
|
@eval_block_passed = false
|
39
44
|
|
45
|
+
warn_about_bare_error if warning_about_bare_error && !negative_expectation
|
40
46
|
return false unless Proc === given_proc
|
41
47
|
|
42
48
|
begin
|
43
49
|
given_proc.call
|
44
50
|
rescue Exception => @actual_error
|
45
|
-
if values_match?(@expected_error, @actual_error)
|
51
|
+
if values_match?(@expected_error, @actual_error) ||
|
52
|
+
values_match?(@expected_error, @actual_error.message)
|
46
53
|
@raised_expected_error = true
|
47
54
|
@with_expected_message = verify_message
|
48
55
|
end
|
49
56
|
end
|
50
57
|
|
51
|
-
|
52
|
-
eval_block if @raised_expected_error && @with_expected_message && @block
|
53
|
-
end
|
58
|
+
eval_block if !negative_expectation && ready_to_eval_block?
|
54
59
|
|
55
60
|
expectation_matched?
|
56
61
|
end
|
@@ -58,7 +63,7 @@ module RSpec
|
|
58
63
|
|
59
64
|
# @private
|
60
65
|
def does_not_match?(given_proc)
|
61
|
-
|
66
|
+
warn_for_false_positives
|
62
67
|
!matches?(given_proc, :negative_expectation) && Proc === given_proc
|
63
68
|
end
|
64
69
|
|
@@ -103,6 +108,10 @@ module RSpec
|
|
103
108
|
@eval_block ? @eval_block_passed : true
|
104
109
|
end
|
105
110
|
|
111
|
+
def ready_to_eval_block?
|
112
|
+
@raised_expected_error && @with_expected_message && @block
|
113
|
+
end
|
114
|
+
|
106
115
|
def eval_block
|
107
116
|
@eval_block = true
|
108
117
|
begin
|
@@ -118,29 +127,62 @@ module RSpec
|
|
118
127
|
values_match?(@expected_message, @actual_error.message.to_s)
|
119
128
|
end
|
120
129
|
|
121
|
-
def
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
130
|
+
def warn_for_false_positives
|
131
|
+
return unless warn_about_potential_false_positives?
|
132
|
+
expression = if expecting_specific_exception? && @expected_message
|
133
|
+
"`expect { }.not_to raise_error(SpecificErrorClass, message)`"
|
134
|
+
elsif expecting_specific_exception?
|
135
|
+
"`expect { }.not_to raise_error(SpecificErrorClass)`"
|
136
|
+
elsif @expected_message
|
137
|
+
"`expect { }.not_to raise_error(message)`"
|
138
|
+
end
|
139
|
+
|
140
|
+
return unless expression
|
141
|
+
|
142
|
+
warn_about_negative_false_positive expression
|
143
|
+
end
|
144
|
+
|
145
|
+
def warn_about_potential_false_positives?
|
146
|
+
RSpec::Expectations.configuration.warn_about_potential_false_positives?
|
147
|
+
end
|
129
148
|
|
130
|
-
|
149
|
+
def warning_about_bare_error
|
150
|
+
@warn_about_bare_error && @block.nil?
|
151
|
+
end
|
131
152
|
|
132
|
-
|
133
|
-
|
153
|
+
def warn_about_bare_error
|
154
|
+
RSpec.warning("Using the `raise_error` matcher without providing a specific " \
|
155
|
+
"error or message risks false positives, since `raise_error` " \
|
156
|
+
"will match when Ruby raises a `NoMethodError`, `NameError` or " \
|
157
|
+
"`ArgumentError`, potentially allowing the expectation to pass " \
|
158
|
+
"without even executing the method you are intending to call. " \
|
159
|
+
"Instead consider providing a specific error class or message. " \
|
160
|
+
"This message can be supressed by setting: " \
|
161
|
+
"`RSpec::Expectations.configuration.warn_about_potential_false_positives = false`")
|
162
|
+
end
|
163
|
+
|
164
|
+
def warn_about_negative_false_positive(expression)
|
165
|
+
RSpec.warning("Using #{expression} risks false positives, since literally " \
|
166
|
+
"any other error would cause the expectation to pass, " \
|
167
|
+
"including those raised by Ruby (e.g. NoMethodError, NameError " \
|
168
|
+
"and ArgumentError), meaning the code you are intending to test " \
|
169
|
+
"may not even get reached. Instead consider using " \
|
170
|
+
"`expect {}.not_to raise_error`. This message can be supressed by setting: " \
|
171
|
+
"`RSpec::Expectations.configuration.warn_about_potential_false_positives = false`")
|
134
172
|
end
|
135
173
|
|
136
174
|
def expected_error
|
137
175
|
case @expected_message
|
138
176
|
when nil
|
139
|
-
|
177
|
+
if RSpec::Support.is_a_matcher?(@expected_error)
|
178
|
+
"Exception with #{description_of(@expected_error)}"
|
179
|
+
else
|
180
|
+
description_of(@expected_error)
|
181
|
+
end
|
140
182
|
when Regexp
|
141
|
-
"#{@expected_error} with message matching #{@expected_message
|
183
|
+
"#{@expected_error} with message matching #{description_of(@expected_message)}"
|
142
184
|
else
|
143
|
-
"#{@expected_error} with #{description_of
|
185
|
+
"#{@expected_error} with #{description_of(@expected_message)}"
|
144
186
|
end
|
145
187
|
end
|
146
188
|
|
@@ -155,7 +197,7 @@ module RSpec
|
|
155
197
|
|
156
198
|
backtrace = format_backtrace(@actual_error.backtrace)
|
157
199
|
[
|
158
|
-
", got #{@actual_error
|
200
|
+
", got #{description_of(@actual_error)} with backtrace:",
|
159
201
|
*backtrace
|
160
202
|
].join("\n # ")
|
161
203
|
end
|
@@ -45,7 +45,7 @@ module RSpec
|
|
45
45
|
# @api private
|
46
46
|
# @return [String]
|
47
47
|
def failure_message
|
48
|
-
"expected #{
|
48
|
+
"expected #{actual_formatted} to respond to #{@failing_method_names.map { |name| description_of(name) }.join(', ')}#{with_arity}"
|
49
49
|
end
|
50
50
|
|
51
51
|
# @api private
|
@@ -82,8 +82,7 @@ module RSpec
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def pp_names
|
85
|
-
|
86
|
-
@names.length == 1 ? "##{@names.first}" : @names.inspect
|
85
|
+
@names.length == 1 ? "##{@names.first}" : description_of(@names)
|
87
86
|
end
|
88
87
|
end
|
89
88
|
end
|
@@ -5,7 +5,11 @@ module RSpec
|
|
5
5
|
# Provides the implementation for `satisfy`.
|
6
6
|
# Not intended to be instantiated directly.
|
7
7
|
class Satisfy < BaseMatcher
|
8
|
-
|
8
|
+
# @private
|
9
|
+
attr_reader :description
|
10
|
+
|
11
|
+
def initialize(description="satisfy block", &block)
|
12
|
+
@description = description
|
9
13
|
@block = block
|
10
14
|
end
|
11
15
|
|
@@ -19,19 +23,13 @@ module RSpec
|
|
19
23
|
# @api private
|
20
24
|
# @return [String]
|
21
25
|
def failure_message
|
22
|
-
"expected #{
|
26
|
+
"expected #{actual_formatted} to #{description}"
|
23
27
|
end
|
24
28
|
|
25
29
|
# @api private
|
26
30
|
# @return [String]
|
27
31
|
def failure_message_when_negated
|
28
|
-
"expected #{
|
29
|
-
end
|
30
|
-
|
31
|
-
# @api private
|
32
|
-
# @return [String]
|
33
|
-
def description
|
34
|
-
"satisfy block"
|
32
|
+
"expected #{actual_formatted} not to #{description}"
|
35
33
|
end
|
36
34
|
end
|
37
35
|
end
|
@@ -26,7 +26,9 @@ module RSpec
|
|
26
26
|
# @return [String]
|
27
27
|
def description
|
28
28
|
return super unless Hash === expected
|
29
|
-
|
29
|
+
english_name = EnglishPhrasing.split_words(self.class.matcher_name)
|
30
|
+
description_of_expected = surface_descriptions_in(expected).inspect
|
31
|
+
"#{english_name} #{description_of_expected}"
|
30
32
|
end
|
31
33
|
|
32
34
|
private
|