File: raise_error.rb

package info (click to toggle)
ruby-rspec 3.13.0c0e0m0s1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,856 kB
  • sloc: ruby: 70,868; sh: 1,423; makefile: 99
file content (271 lines) | stat: -rw-r--r-- 9,862 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
module RSpec
  module Matchers
    module BuiltIn
      # @api private
      # Provides the implementation for `raise_error`.
      # Not intended to be instantiated directly.
      # rubocop:disable Metrics/ClassLength
      # rubocop:disable Lint/RescueException
      class RaiseError
        include Composable

        # Used as a sentinel value to be able to tell when the user did not pass an
        # argument. We can't use `nil` for that because we need to warn when `nil` is
        # passed in a different way. It's an Object, not a Module, since Module's `===`
        # does not evaluate to true when compared to itself.
        UndefinedValue = Object.new.freeze

        def initialize(expected_error_or_message, expected_message, &block)
          @block = block
          @actual_error = nil
          @warn_about_bare_error = UndefinedValue === expected_error_or_message
          @warn_about_nil_error = expected_error_or_message.nil?

          case expected_error_or_message
          when nil, UndefinedValue
            @expected_error = Exception
            @expected_message = expected_message
          when String
            @expected_error = Exception
            @expected_message = expected_error_or_message
          else
            @expected_error = expected_error_or_message
            @expected_message = expected_message
          end
        end

        # @api public
        # Specifies the expected error message.
        def with_message(expected_message)
          raise_message_already_set if @expected_message
          @warn_about_bare_error = false
          @expected_message = expected_message
          self
        end

        # rubocop:disable Metrics/MethodLength
        # @private
        def matches?(given_proc, negative_expectation=false, &block)
          @given_proc = given_proc
          @block ||= block
          @raised_expected_error = false
          @with_expected_message = false
          @eval_block = false
          @eval_block_passed = false

          return false unless Proc === given_proc

          begin
            given_proc.call
          rescue Exception => @actual_error
            if values_match?(@expected_error, @actual_error) ||
               values_match?(@expected_error, actual_error_message)
              @raised_expected_error = true
              @with_expected_message = verify_message
            end
          end

          unless negative_expectation
            warn_about_bare_error! if warn_about_bare_error?
            warn_about_nil_error! if warn_about_nil_error?
            eval_block if ready_to_eval_block?
          end

          expectation_matched?
        end
        # rubocop:enable Metrics/MethodLength

        # @private
        def does_not_match?(given_proc)
          warn_for_negative_false_positives!
          !matches?(given_proc, :negative_expectation) && Proc === given_proc
        end

        # @private
        def supports_block_expectations?
          true
        end

        # @private
        def supports_value_expectations?
          false
        end

        # @private
        def expects_call_stack_jump?
          true
        end

        # @api private
        # @return [String]
        def failure_message
          @eval_block ? actual_error_message : "expected #{expected_error}#{given_error}"
        end

        # @api private
        # @return [String]
        def failure_message_when_negated
          "expected no #{expected_error}#{given_error}"
        end

        # @api private
        # @return [String]
        def description
          "raise #{expected_error}"
        end

      private

        def actual_error_message
          return nil unless @actual_error

          @actual_error.respond_to?(:original_message) ? @actual_error.original_message : @actual_error.message
        end

        def expectation_matched?
          error_and_message_match? && block_matches?
        end

        def error_and_message_match?
          @raised_expected_error && @with_expected_message
        end

        def block_matches?
          @eval_block ? @eval_block_passed : true
        end

        def ready_to_eval_block?
          @raised_expected_error && @with_expected_message && @block
        end

        def eval_block
          @eval_block = true
          begin
            @block[@actual_error]
            @eval_block_passed = true
          rescue Exception => err
            @actual_error = err
          end
        end

        def verify_message
          return true if @expected_message.nil?
          values_match?(@expected_message, actual_error_message.to_s)
        end

        def warn_for_negative_false_positives!
          expression = if expecting_specific_exception? && @expected_message
                         "`expect { }.not_to raise_error(SpecificErrorClass, message)`"
                       elsif expecting_specific_exception?
                         "`expect { }.not_to raise_error(SpecificErrorClass)`"
                       elsif @expected_message
                         "`expect { }.not_to raise_error(message)`"
                       elsif @warn_about_nil_error
                         "`expect { }.not_to raise_error(nil)`"
                       end

          return unless expression

          warn_about_negative_false_positive! expression
        end

        def handle_warning(message)
          RSpec::Expectations.configuration.false_positives_handler.call(message)
        end

        def warn_about_bare_error?
          @warn_about_bare_error && @block.nil?
        end

        def warn_about_nil_error?
          @warn_about_nil_error
        end

        def warn_about_bare_error!
          handle_warning("Using the `raise_error` matcher without providing a specific " \
                         "error or message risks false positives, since `raise_error` " \
                         "will match when Ruby raises a `NoMethodError`, `NameError` or " \
                         "`ArgumentError`, potentially allowing the expectation to pass " \
                         "without even executing the method you are intending to call. " \
                         "#{warning}"\
                         "Instead consider providing a specific error class or message. " \
                         "This message can be suppressed by setting: " \
                         "`RSpec::Expectations.configuration.on_potential_false" \
                         "_positives = :nothing`")
        end

        def warn_about_nil_error!
          handle_warning("Using the `raise_error` matcher with a `nil` error is probably " \
                         "unintentional, it risks false positives, since `raise_error` " \
                         "will match when Ruby raises a `NoMethodError`, `NameError` or " \
                         "`ArgumentError`, potentially allowing the expectation to pass " \
                         "without even executing the method you are intending to call. " \
                         "#{warning}"\
                         "Instead consider providing a specific error class or message. " \
                         "This message can be suppressed by setting: " \
                         "`RSpec::Expectations.configuration.on_potential_false" \
                         "_positives = :nothing`")
        end

        def warn_about_negative_false_positive!(expression)
          handle_warning("Using #{expression} risks false positives, since literally " \
                         "any other error would cause the expectation to pass, " \
                         "including those raised by Ruby (e.g. `NoMethodError`, `NameError` " \
                         "and `ArgumentError`), meaning the code you are intending to test " \
                         "may not even get reached. Instead consider using " \
                         "`expect { }.not_to raise_error` or `expect { }.to raise_error" \
                         "(DifferentSpecificErrorClass)`. This message can be suppressed by " \
                         "setting: `RSpec::Expectations.configuration.on_potential_false" \
                         "_positives = :nothing`")
        end

        def expected_error
          case @expected_message
          when nil
            if RSpec::Support.is_a_matcher?(@expected_error)
              "Exception with #{description_of(@expected_error)}"
            else
              description_of(@expected_error)
            end
          when Regexp
            "#{@expected_error} with message matching #{description_of(@expected_message)}"
          else
            "#{@expected_error} with #{description_of(@expected_message)}"
          end
        end

        def format_backtrace(backtrace)
          formatter = Matchers.configuration.backtrace_formatter
          formatter.format_backtrace(backtrace)
        end

        def given_error
          return " but was not given a block" unless Proc === @given_proc
          return " but nothing was raised" unless @actual_error

          backtrace = format_backtrace(@actual_error.backtrace)
          [
            ", got #{description_of(@actual_error)} with backtrace:",
            *backtrace
          ].join("\n  # ")
        end

        def expecting_specific_exception?
          @expected_error != Exception
        end

        def raise_message_already_set
          raise "`expect { }.to raise_error(message).with_message(message)` is not valid. " \
                'The matcher only allows the expected message to be specified once'
        end

        def warning
          warning = "Actual error raised was #{description_of(@actual_error)}. "
          warning if @actual_error
        end
      end
      # rubocop:enable Lint/RescueException
      # rubocop:enable Metrics/ClassLength
    end
  end
end