Skip to content

Commit

Permalink
Make count expectation of yield_control generic
Browse files Browse the repository at this point in the history
  • Loading branch information
marcandre committed Mar 17, 2020
1 parent 6e9f6c3 commit 14b240a
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 93 deletions.
129 changes: 129 additions & 0 deletions lib/rspec/matchers/built_in/count_expectation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
module RSpec
module Matchers
module BuiltIn
# @api private
# Provides the implementation for `yield_control`.
# Not intended to be instantiated directly.
module CountExpectation
def initialize(*args)
@count_expectation_type = @count_expected_count = nil
super(*args)
end

# @api public
# Specifies that the method is expected to yield once.
def once
exactly(1)
self
end

# @api public
# Specifies that the method is expected to yield twice.
def twice
exactly(2)
self
end

# @api public
# Specifies that the method is expected to yield thrice.
def thrice
exactly(3)
self
end

# @api public
# Specifies that the method is expected to yield the given number of times.
def exactly(number)
set_expected_count(:==, number)
self
end

# @api public
# Specifies the maximum number of times the method is expected to yield
def at_most(number)
set_expected_count(:<=, number)
self
end

# @api public
# Specifies the minimum number of times the method is expected to yield
def at_least(number)
set_expected_count(:>=, number)
self
end

# @api public
# No-op. Provides syntactic sugar.
def times
self
end

# @private
def matches?(actual_count)
expected_count_matches?(actual_count)
end

# @private
def does_not_match?(actual_count)
!expected_count_matches?(actual_count)
end

private

def expected_count_matches?(actual_count)
@actual_count = actual_count
@actual_count.__send__(@count_expectation_type || :>=, @count_expected_count || 1)
end

def has_expected_count?
!!@count_expectation_type
end

def set_expected_count(relativity, n)
raise "Multiple count constraints are not supported" if @count_expectation_type

@count_expectation_type = relativity
@count_expected_count = count_constraint_to_number(n)
end

def count_constraint_to_number(n)
case n
when Numeric then n
when :once then 1
when :twice then 2
when :thrice then 3
else raise ArgumentError,
"Expected a number, :once, :twice or :thrice," \
" but got #{n}"
end
end

def count_expectation_description
"#{human_readable_expectation_type}#{human_readable_count(@count_expected_count)}"
end

def count_failure_reason(action)
"#{count_expectation_description}" \
" but #{action}#{human_readable_count(@actual_count)}"
end

def human_readable_expectation_type
case @count_expectation_type
when :<= then ' at most'
when :>= then ' at least'
else ''
end
end

def human_readable_count(count)
case count
when nil then ''
when 1 then ' once'
when 2 then ' twice'
else " #{count} times"
end
end
end
end
end
end
98 changes: 5 additions & 93 deletions lib/rspec/matchers/built_in/yield.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'rspec/matchers/built_in/count_expectation'

RSpec::Support.require_rspec_support 'method_signature_verifier'

module RSpec
Expand Down Expand Up @@ -97,66 +99,13 @@ def assert_valid_expect_block!
# Provides the implementation for `yield_control`.
# Not intended to be instantiated directly.
class YieldControl < BaseMatcher
def initialize
@already_set = false
at_least(:once)
@already_set = false
end

# @api public
# Specifies that the method is expected to yield once.
def once
exactly(1)
self
end

# @api public
# Specifies that the method is expected to yield twice.
def twice
exactly(2)
self
end

# @api public
# Specifies that the method is expected to yield thrice.
def thrice
exactly(3)
self
end

# @api public
# Specifies that the method is expected to yield the given number of times.
def exactly(number)
set_expected_yields_count(:==, number)
self
end

# @api public
# Specifies the maximum number of times the method is expected to yield
def at_most(number)
set_expected_yields_count(:<=, number)
self
end

# @api public
# Specifies the minimum number of times the method is expected to yield
def at_least(number)
set_expected_yields_count(:>=, number)
self
end

# @api public
# No-op. Provides syntactic sugar.
def times
self
end
include CountExpectation

# @private
def matches?(block)
@probe = YieldProbe.probe(block)
return false unless @probe.has_block?

@probe.num_yields.__send__(@expectation_type, @expected_yields_count)
super(@probe.num_yields)
end

# @private
Expand All @@ -183,46 +132,9 @@ def supports_block_expectations?

private

def set_expected_yields_count(relativity, n)
raise "Multiple count constraints are not supported" if @already_set

@expectation_type = relativity
@expected_yields_count = count_constraint_to_number(n)
@already_set = true
end

def count_constraint_to_number(n)
case n
when Numeric then n
when :once then 1
when :twice then 2
when :thrice then 3
else raise ArgumentError,
"Expected a number, :once, :twice or :thrice," \
" but got #{n}"
end
end

def failure_reason
return ' but was not a block' unless @probe.has_block?
" #{human_readable_expectation_type}#{human_readable_count(@expected_yields_count)}" \
" but yielded #{human_readable_count(@probe.num_yields)}"
end

def human_readable_expectation_type
case @expectation_type
when :<= then 'at most '
when :>= then 'at least '
else ''
end
end

def human_readable_count(count)
case count
when 1 then 'once'
when 2 then 'twice'
else "#{count} times"
end
count_failure_reason('yielded')
end
end

Expand Down

0 comments on commit 14b240a

Please sign in to comment.