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 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
|
module RSpec
module Matchers
module BuiltIn
class YieldProbe
def self.probe(block)
probe = new
assert_valid_expect_block!(block)
block.call(probe)
probe.assert_used!
probe
end
attr_accessor :num_yields, :yielded_args
def initialize
@used = false
self.num_yields, self.yielded_args = 0, []
end
def to_proc
@used = true
probe = self
Proc.new do |*args|
probe.num_yields += 1
probe.yielded_args << args
end
end
def single_yield_args
yielded_args.first
end
def yielded_once?(matcher_name)
case num_yields
when 1 then true
when 0 then false
else
raise "The #{matcher_name} matcher is not designed to be used with a " +
"method that yields multiple times. Use the yield_successive_args " +
"matcher for that case."
end
end
def successive_yield_args
yielded_args.map do |arg_array|
arg_array.size == 1 ? arg_array.first : arg_array
end
end
def assert_used!
return if @used
raise "You must pass the argument yielded to your expect block on " +
"to the method-under-test as a block. It acts as a probe that " +
"allows the matcher to detect whether or not the method-under-test " +
"yields, and, if so, how many times, and what the yielded arguments " +
"are."
end
def self.assert_valid_expect_block!(block)
return if block.arity == 1
raise "Your expect block must accept an argument to be used with this " +
"matcher. Pass the argument as a block on to the method you are testing."
end
end
class YieldControl < BaseMatcher
def initialize
@expectation_type = nil
@expected_yields_count = nil
end
def matches?(block)
probe = YieldProbe.probe(block)
if @expectation_type
probe.num_yields.send(@expectation_type, @expected_yields_count)
else
probe.yielded_once?(:yield_control)
end
end
def once
exactly(1)
self
end
def twice
exactly(2)
self
end
def exactly(number)
set_expected_yields_count(:==, number)
self
end
def at_most(number)
set_expected_yields_count(:<=, number)
self
end
def at_least(number)
set_expected_yields_count(:>=, number)
self
end
def times
self
end
def failure_message_for_should
'expected given block to yield control'.tap do |failure_message|
failure_message << relativity_failure_message
end
end
def failure_message_for_should_not
'expected given block not to yield control'.tap do |failure_message|
failure_message << relativity_failure_message
end
end
private
def set_expected_yields_count(relativity, n)
@expectation_type = relativity
@expected_yields_count = case n
when Numeric then n
when :once then 1
when :twice then 2
end
end
def relativity_failure_message
return '' unless @expected_yields_count
" #{human_readable_expecation_type}#{human_readable_count}"
end
def human_readable_expecation_type
case @expectation_type
when :<= then 'at most '
when :>= then 'at least '
else ''
end
end
def human_readable_count
case @expected_yields_count
when 1 then "once"
when 2 then "twice"
else "#{@expected_yields_count} times"
end
end
end
class YieldWithNoArgs < BaseMatcher
def matches?(block)
@probe = YieldProbe.probe(block)
@probe.yielded_once?(:yield_with_no_args) && @probe.single_yield_args.empty?
end
def failure_message_for_should
"expected given block to yield with no arguments, but #{failure_reason}"
end
def failure_message_for_should_not
"expected given block not to yield with no arguments, but did"
end
private
def failure_reason
if @probe.num_yields.zero?
"did not yield"
else
"yielded with arguments: #{@probe.single_yield_args.inspect}"
end
end
end
class YieldWithArgs
def initialize(*args)
@expected = args
end
def matches?(block)
@probe = YieldProbe.probe(block)
@actual = @probe.single_yield_args
@probe.yielded_once?(:yield_with_args) && args_match?
end
alias == matches?
def failure_message_for_should
"expected given block to yield with arguments, but #{positive_failure_reason}"
end
def failure_message_for_should_not
"expected given block not to yield with arguments, but #{negative_failure_reason}"
end
def description
desc = "yield with args"
desc << "(" + @expected.map { |e| e.inspect }.join(", ") + ")" unless @expected.empty?
desc
end
private
def positive_failure_reason
if @probe.num_yields.zero?
"did not yield"
else
@positive_args_failure
end
end
def negative_failure_reason
if all_args_match?
"yielded with expected arguments" +
"\nexpected not: #{@expected.inspect}" +
"\n got: #{@actual.inspect} (compared using === and ==)"
else
"did"
end
end
def args_match?
if @expected.empty? # expect {...}.to yield_with_args
@positive_args_failure = "yielded with no arguments" if @actual.empty?
return !@actual.empty?
end
unless match = all_args_match?
@positive_args_failure = "yielded with unexpected arguments" +
"\nexpected: #{@expected.inspect}" +
"\n got: #{@actual.inspect} (compared using === and ==)"
end
match
end
def all_args_match?
return false if @expected.size != @actual.size
@expected.zip(@actual).all? do |expected, actual|
expected === actual || actual == expected
end
end
end
class YieldSuccessiveArgs
def initialize(*args)
@expected = args
end
def matches?(block)
@probe = YieldProbe.probe(block)
@actual = @probe.successive_yield_args
args_match?
end
alias == matches?
def failure_message_for_should
"expected given block to yield successively with arguments, but yielded with unexpected arguments" +
"\nexpected: #{@expected.inspect}" +
"\n got: #{@actual.inspect} (compared using === and ==)"
end
def failure_message_for_should_not
"expected given block not to yield successively with arguments, but yielded with expected arguments" +
"\nexpected not: #{@expected.inspect}" +
"\n got: #{@actual.inspect} (compared using === and ==)"
end
def description
desc = "yield successive args"
desc << "(" + @expected.map { |e| e.inspect }.join(", ") + ")"
desc
end
private
def args_match?
return false if @expected.size != @actual.size
@expected.zip(@actual).all? do |expected, actual|
expected === actual || actual == expected
end
end
end
end
end
end
|