Skip to content

Commit

Permalink
feat: implement explicit block support
Browse files Browse the repository at this point in the history
  • Loading branch information
doudou committed Jul 18, 2024
1 parent c5c6c73 commit 94ea6cc
Show file tree
Hide file tree
Showing 18 changed files with 115 additions and 131 deletions.
3 changes: 1 addition & 2 deletions flexmock.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ spec = Gem::Specification.new do |s|
interface is simple, it is very flexible.
}

s.required_ruby_version = ">= 2.2"
s.required_ruby_version = ">= 3.0"

s.license = 'MIT'

Expand All @@ -19,7 +19,6 @@ spec = Gem::Specification.new do |s|
s.add_development_dependency 'minitest', ">= 5.0"
s.add_development_dependency 'rake'
s.add_development_dependency 'simplecov', '>= 0.11.0'
s.add_development_dependency 'coveralls'

#### Which files are to be included in this gem? Everything! (Except CVS directories.)

Expand Down
15 changes: 0 additions & 15 deletions lib/flexmock/argument_matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,4 @@ def inspect
"ducktype(#{@methods.map{|m| m.inspect}.join(',')})"
end
end

####################################################################
# Match objects that implement all the methods in +methods+.
class OptionalProcMatcher
def initialize
end
def ===(target)
ArgumentMatching.missing?(target) || Proc === target
end
def inspect
"optional_proc"
end
end
OPTIONAL_PROC_MATCHER = OptionalProcMatcher.new

end
11 changes: 9 additions & 2 deletions lib/flexmock/argument_matching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ module ArgumentMatching

MISSING_ARG = Object.new

def all_match?(expected_args, expected_kw, actual_args, actual_kw)
def all_match?(expected_args, expected_kw, expected_block, actual_args, actual_kw, actual_block)
all_match_args?(expected_args, actual_args) &&
all_match_kw?(expected_kw, actual_kw)
all_match_kw?(expected_kw, actual_kw) &&
all_match_block?(expected_block, actual_block)
end

def all_match_args?(expected_args, actual_args)
Expand Down Expand Up @@ -42,6 +43,12 @@ def all_match_kw?(expected_kw, actual_kw)
true
end

def all_match_block?(expected_block, actual_block)
return true if expected_block.nil?

!(expected_block ^ actual_block)
end

# Does the expected argument match the corresponding actual value.
def match?(expected, actual)
expected === actual ||
Expand Down
4 changes: 0 additions & 4 deletions lib/flexmock/argument_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ def hsh(hash)
def ducktype(*methods)
DuckMatcher.new(methods)
end

def optional_proc
OPTIONAL_PROC_MATCHER
end
end
extend ArgumentTypes

Expand Down
13 changes: 9 additions & 4 deletions lib/flexmock/call_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@

class FlexMock

CallRecord = Struct.new(:method_name, :args, :kw, :block_given, :expectation) do
CallRecord = Struct.new(:method_name, :args, :kw, :block, :expectation) do
def matches?(sym, expected_args, expected_kw, options)
method_name == sym &&
ArgumentMatching.all_match?(expected_args, expected_kw, args, kw) &&
ArgumentMatching.all_match_args?(expected_args, args) &&
ArgumentMatching.all_match_kw?(expected_kw, kw) &&
matches_block?(options[:with_block])
end

private

def matches_block?(block_option)
block_option.nil? ||
(block_option && block_given) ||
(!block_option && !block_given)
(block_option && block) ||
(!block_option && !block)
end

def block_given
block
end
end

Expand Down
11 changes: 5 additions & 6 deletions lib/flexmock/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,13 @@ def by_default
def method_missing(sym, *args, **kw, &block)
FlexMock.verify_mocking_allowed!

enhanced_args = block_given? ? args + [block] : args
call_record = CallRecord.new(sym, enhanced_args, kw, block_given?)
call_record = CallRecord.new(sym, args, kw, block)
@calls << call_record
flexmock_wrap do
if flexmock_closed?
FlexMock.undefined
elsif exp = flexmock_expectations_for(sym)
exp.call(enhanced_args, kw, call_record)
exp.call(args, kw, block, call_record)
elsif @base_class && @base_class.flexmock_defined?(sym)
FlexMock.undefined
elsif @ignore_missing
Expand All @@ -167,9 +166,9 @@ def respond_to?(sym, *args)
end

# Find the mock expectation for method sym and arguments.
def flexmock_find_expectation(method_name, *args, **kw) # :nodoc:
def flexmock_find_expectation(method_name, *args, **kw, &block) # :nodoc:
if exp = flexmock_expectations_for(method_name)
exp.find_expectation(args, kw)
exp.find_expectation(args, kw, block)
end
end

Expand Down Expand Up @@ -214,7 +213,7 @@ def flexmock_invoke_original(method_name, args, kw = {})
# Override the built-in +method+ to include the mocked methods.
def method(method_name)
if (expectations = flexmock_expectations_for(method_name))
->(*args, **kw) { expectations.call(args, kw) }
->(*args, **kw, &block) { expectations.call(args, kw, block) }
else
super
end
Expand Down
51 changes: 34 additions & 17 deletions lib/flexmock/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def initialize(mock, sym, location)
@count_validators = []
@signature_validator = SignatureValidator.new(self)
@count_validator_class = ExactCountValidator
@with_block = nil
@actual_count = 0
@return_value = nil
@return_queue = []
Expand Down Expand Up @@ -86,48 +87,47 @@ def validate_eligible
FlexMock.framework_adapter.check(e.message) { false }
end

def validate_signature(args, kw)
@signature_validator.validate(args, kw)
def validate_signature(args, kw, block)
@signature_validator.validate(args, kw, block)
rescue SignatureValidator::ValidationFailed => e
FlexMock.framework_adapter.check(e.message) { false }
end

# Verify the current call with the given arguments matches the
# expectations recorded in this object.
def verify_call(args, kw)
def verify_call(args, kw, block)
validate_eligible
validate_order
validate_signature(args, kw)
validate_signature(args, kw, block)
@actual_count += 1
perform_yielding(args)
return_value(args, kw)
perform_yielding(block)
return_value(args, kw, block)
end

# Public return value (odd name to avoid accidental use as a
# constraint).
def _return_value(args, kw) # :nodoc:
return_value(args, kw)
def _return_value(args, kw, block) # :nodoc:
return_value(args, kw, block)
end

# Find the return value for this expectation. (private version)
def return_value(args, kw)
def return_value(args, kw, block)
case @return_queue.size
when 0
block = lambda { |*a| @return_value }
ret_block = lambda { |*, **| @return_value }
when 1
block = @return_queue.first
ret_block = @return_queue.first
else
block = @return_queue.shift
ret_block = @return_queue.shift
end
block.call(*args, **kw)
ret_block.call(*args, **kw, &block)
end
private :return_value

# Yield stored values to any blocks given.
def perform_yielding(args)
def perform_yielding(block)
@return_value = nil
unless @yield_queue.empty?
block = args.last
values = (@yield_queue.size == 1) ? @yield_queue.first : @yield_queue.shift
if block && block.respond_to?(:call)
values.each do |v|
Expand Down Expand Up @@ -173,8 +173,8 @@ def flexmock_verify

# Does the argument list match this expectation's argument
# specification.
def match_args(args, kw)
ArgumentMatching.all_match?(@expected_args, @expected_kw, args, kw)
def match_args(args, kw, block)
ArgumentMatching.all_match?(@expected_args, @expected_kw, @expected_block, args, kw, block)
end

# Declare that the method should expect the given argument list.
Expand Down Expand Up @@ -218,6 +218,23 @@ def with_kw_args(kw)
self
end

# Declare that the call should have a block
def with_block
@expected_block = true
self
end

# Declare that the call should have a block
def with_no_block
@expected_block = false
self
end

def with_optional_block
@expected_block = nil
self
end

# Validate general parameters on the call signature
def with_signature(
required_arguments: 0, optional_arguments: 0, splat: false,
Expand Down
2 changes: 1 addition & 1 deletion lib/flexmock/expectation_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def create_demeter_chain(mock, names)
names.each do |name|
exp = mock.flexmock_find_expectation(name)
if exp
next_mock = exp._return_value([], {})
next_mock = exp._return_value([], {}, nil)
check_proper_mock(next_mock, name)
else
next_mock = container.flexmock("demeter_#{name}")
Expand Down
18 changes: 9 additions & 9 deletions lib/flexmock/expectation_director.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ def initialize(sym)
# but at least we will get a good failure message). Finally,
# check for expectations that don't have any argument matching
# criteria.
def call(args, kw, call_record=nil)
exp = find_expectation(args, kw)
def call(args, kw, block, call_record=nil)
exp = find_expectation(args, kw, block)
call_record.expectation = exp if call_record
FlexMock.check(
proc { "no matching handler found for " +
FlexMock.format_call(@sym, args, kw) +
"\nDefined expectations:\n " +
@expectations.map(&:description).join("\n ") }
) { !exp.nil? }
returned_value = exp.verify_call(args, kw)
returned_value = exp.verify_call(args, kw, block)
returned_value
end

Expand All @@ -54,11 +54,11 @@ def <<(expectation)
end

# Find an expectation matching the given arguments.
def find_expectation(args, kw) # :nodoc:
def find_expectation(args, kw, block) # :nodoc:
if @expectations.empty?
find_expectation_in(@defaults, args, kw)
find_expectation_in(@defaults, args, kw, block)
else
find_expectation_in(@expectations, args, kw)
find_expectation_in(@expectations, args, kw, block)
end
end

Expand All @@ -84,9 +84,9 @@ def defaultify_expectation(exp) # :nodoc:

private

def find_expectation_in(expectations, args, kw)
expectations.find { |e| e.match_args(args, kw) && e.eligible? } ||
expectations.find { |e| e.match_args(args, kw) }
def find_expectation_in(expectations, args, kw, block)
expectations.find { |e| e.match_args(args, kw, block) && e.eligible? } ||
expectations.find { |e| e.match_args(args, kw, block) }
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/flexmock/partial_mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ def flexmock_define_expectation(location, *args, **kw)
end
end

def flexmock_find_expectation(*args, **kw)
@mock.flexmock_find_expectation(*args, **kw)
def flexmock_find_expectation(*args, **kw, &block)
@mock.flexmock_find_expectation(*args, **kw, &block)
end

def add_mock_method(method_name)
Expand Down
35 changes: 5 additions & 30 deletions lib/flexmock/validators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,44 +212,19 @@ def describe
#
# @param [Array] args
# @raise ValidationFailed
def validate(args, kw)
args = args.dup
def validate(args, kw, block)
kw ||= Hash.new

last_is_proc = false
begin
if args.last.kind_of?(Proc)
args.pop
last_is_proc = true
end
rescue NoMethodError
end

if expects_keyword_arguments? && requires_keyword_arguments? && kw.empty?
raise ValidationFailed, "#{@exp} expects keyword arguments but none were provided"
end

# There is currently no way to disambiguate "given a block" from "given a
# proc as last argument" ... give some leeway in this case
positional_count = args.size

if required_arguments > positional_count
if requires_keyword_arguments?
raise ValidationFailed, "#{@exp} expects at least #{required_arguments} positional arguments but got only #{positional_count}"
end

if (required_arguments - positional_count) == 1 && last_is_proc
last_is_proc = false
positional_count += 1
else
raise ValidationFailed, "#{@exp} expects at least #{required_arguments} positional arguments but got only #{positional_count}"
end
if required_arguments > args.size
raise ValidationFailed, "#{@exp} expects at least #{required_arguments} positional arguments but got only #{args.size}"
end

if !splat? && (required_arguments + optional_arguments) < positional_count
if !last_is_proc || (required_arguments + optional_arguments) < positional_count - 1
raise ValidationFailed, "#{@exp} expects at most #{required_arguments + optional_arguments} positional arguments but got #{positional_count}"
end
if !splat? && (required_arguments + optional_arguments) < args.size
raise ValidationFailed, "#{@exp} expects at most #{required_arguments + optional_arguments} positional arguments but got #{args.size}"
end

missing_keyword_arguments = required_keyword_arguments.
Expand Down
2 changes: 1 addition & 1 deletion lib/flexmock/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class FlexMock
VERSION = "2.3.8"
VERSION = "3.0.0"
end
4 changes: 2 additions & 2 deletions test/assert_spy_called_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ def test_assert_rejects_incorrect_type
def test_assert_detects_blocks
spy.foo { }
spy.bar
assert_spy_called spy, :foo, Proc
assert_spy_called spy, :bar
assert_spy_called spy, {with_block: true}, :foo
assert_spy_called spy, {with_block: false}, :bar
end

def test_assert_detects_any_args
Expand Down
2 changes: 1 addition & 1 deletion test/deprecated_methods_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_handle_no_block

def test_called_with_block
called = false
s { @mock.mock_handle(:blip) { |block| block.call } }
s { @mock.mock_handle(:blip) { |&block| block.call } }
@mock.blip { called = true }
assert called, "Block to blip should be called"
end
Expand Down
Loading

0 comments on commit 94ea6cc

Please sign in to comment.