From e4290b8d29aa87c8b609d0f0ef7f29669ed3f35d Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 18 Mar 2020 20:15:38 +0000 Subject: [PATCH 1/4] Add improved 'did not yield' message and verify errors --- lib/rspec/matchers/built_in/yield.rb | 2 ++ spec/rspec/matchers/built_in/yield_spec.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/rspec/matchers/built_in/yield.rb b/lib/rspec/matchers/built_in/yield.rb index 9879d2db1..79f7680a2 100644 --- a/lib/rspec/matchers/built_in/yield.rb +++ b/lib/rspec/matchers/built_in/yield.rb @@ -202,6 +202,8 @@ def count_constraint_to_number(n) def failure_reason return ' but was not a block' unless @probe.has_block? + return "#{human_readable_expectation_type}#{human_readable_count(@expected_yields_count)} but did not yield" if @probe.num_yields.zero? + "#{human_readable_expectation_type}#{human_readable_count(@expected_yields_count)}" \ " but yielded#{human_readable_count(@probe.num_yields)}" end diff --git a/spec/rspec/matchers/built_in/yield_spec.rb b/spec/rspec/matchers/built_in/yield_spec.rb index f315ee6b5..102e15fa3 100644 --- a/spec/rspec/matchers/built_in/yield_spec.rb +++ b/spec/rspec/matchers/built_in/yield_spec.rb @@ -61,6 +61,24 @@ def each_arg(*args, &block) }.to fail_with(/expected given block to yield control but/) end + it 'fails if the block does not yield the correct number of times' do + expect { + expect { |b| 0.times.each(&b) }.to yield_control.at_least(:once) + }.to fail_with(/expected given block to yield control at least once but did not yield/) + + expect { + expect { |b| 2.times.each(&b) }.to yield_control.at_most(:once) + }.to fail_with(/expected given block to yield control at most once but yielded twice/) + + expect { + expect { |b| 1.times.each(&b) }.to yield_control.at_least(:twice) + }.to fail_with(/expected given block to yield control at least twice but yielded once/) + + expect { + expect { |b| 0.times.each(&b) }.to yield_control + }.to fail_with(/expected given block to yield control but did not yield/) + end + it 'does not return a meaningful value from the block' do val = nil expect { |b| val = _yield_with_args(&b) }.to yield_control From fac527e0fc90325eb16abd009a5a2843103f917a Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 18 Mar 2020 20:29:16 +0000 Subject: [PATCH 2/4] Add support for multiple yield control combinations --- lib/rspec/matchers/built_in/yield.rb | 50 ++++++++++++++++++++-- spec/rspec/matchers/built_in/yield_spec.rb | 12 +++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/lib/rspec/matchers/built_in/yield.rb b/lib/rspec/matchers/built_in/yield.rb index 79f7680a2..818538e91 100644 --- a/lib/rspec/matchers/built_in/yield.rb +++ b/lib/rspec/matchers/built_in/yield.rb @@ -96,7 +96,7 @@ def assert_valid_expect_block! # @api private # Provides the implementation for `yield_control`. # Not intended to be instantiated directly. - class YieldControl < BaseMatcher + class YieldControl < BaseMatcher # rubocop:disable ClassLength def initialize @expectation_type = @expected_yields_count = nil end @@ -154,6 +154,8 @@ def matches?(block) @probe = YieldProbe.probe(block) return false unless @probe.has_block? return @probe.num_yields > 0 unless @expectation_type + return @expected_yields_count.cover?(@probe.num_yields) if @expectation_type == :<=> + @probe.num_yields.__send__(@expectation_type, @expected_yields_count) end @@ -182,10 +184,48 @@ def supports_block_expectations? private def set_expected_yields_count(relativity, n) - raise "Multiple count constraints are not supported" if @expectation_type + raise_unsupported_yield_expectation if unsupported_yield_expectation?(relativity) + + count = count_constraint_to_number(n) + + if @expectation_type == :<= && relativity == :>= + raise_impossible_yield_expectation(count) if count > @expected_yields_count + @expectation_type = :<=> + @expected_yields_count = count..@expected_yields_count + elsif @expectation_type == :>= && relativity == :<= + raise_impossible_yield_expectation(count) if count < @expected_yields_count + @expectation_type = :<=> + @expected_yields_count = @expected_yields_count..count + else + @expectation_type = relativity + @expected_yields_count = count + end + end + + def raise_impossible_yield_expectation(count) + text = + case @expectation_type + when :<= then "at_least(#{count}).at_most(#{@expected_yields_count})" + when :>= then "at_least(#{@expected_yields_count}).at_most(#{count})" + end + raise ArgumentError, "The constraint #{text} is not possible" + end + + def raise_unsupported_yield_expectation + text = + case @expectation_type + when :<= then "at_least" + when :>= then "at_most" + when :<=> then "at_least/at_most combination" + else "count" + end + raise ArgumentError, "Multiple #{text} constraints are not supported" + end - @expectation_type = relativity - @expected_yields_count = count_constraint_to_number(n) + def unsupported_yield_expectation?(relativity) + return true if @expectation_type == :== + return true if @expectation_type == :<=> + (@expectation_type == :<= && relativity == :<=) || (@expectation_type == :>= && relativity == :>=) end def count_constraint_to_number(n) @@ -212,12 +252,14 @@ def human_readable_expectation_type case @expectation_type when :<= then ' at most' when :>= then ' at least' + when :<=> then ' between' else '' end end def human_readable_count(count) case count + when Range then " #{count.first} and #{count.last} times" when nil then '' when 1 then ' once' when 2 then ' twice' diff --git a/spec/rspec/matchers/built_in/yield_spec.rb b/spec/rspec/matchers/built_in/yield_spec.rb index 102e15fa3..6075f4f12 100644 --- a/spec/rspec/matchers/built_in/yield_spec.rb +++ b/spec/rspec/matchers/built_in/yield_spec.rb @@ -74,6 +74,10 @@ def each_arg(*args, &block) expect { |b| 1.times.each(&b) }.to yield_control.at_least(:twice) }.to fail_with(/expected given block to yield control at least twice but yielded once/) + expect { + expect { |b| 3.times.each(&b) }.to yield_control.at_least(:once).at_most(2) + }.to fail_with(/expected given block to yield control between 1 and 2 times but yielded 3 times/) + expect { expect { |b| 0.times.each(&b) }.to yield_control }.to fail_with(/expected given block to yield control but did not yield/) @@ -89,10 +93,14 @@ def each_arg(*args, &block) expect { yield_control.exactly('2') }.to raise_error(ArgumentError) expect { yield_control.at_least(:trice_with_typo) }.to raise_error(ArgumentError) expect { yield_control.at_most(nil) }.to raise_error(ArgumentError) + expect { yield_control.at_least(2).at_least(1) }.to raise_error(ArgumentError) + expect { yield_control.at_most(2).at_most(1) }.to raise_error(ArgumentError) + expect { yield_control.at_most(2).at_least(1).at_most(1) }.to raise_error(ArgumentError) + expect { yield_control.at_most(1).at_least(2) }.to raise_error(ArgumentError) + expect { yield_control.at_least(2).at_most(1) }.to raise_error(ArgumentError) end - it 'is yet to support multiple calls to compatible count constraints' do - pending + it 'is supports multiple calls to compatible count constraints' do expect { |b| 1.upto(4, &b) }.to yield_control.at_least(3).at_most(4).times expect { |b| 1.upto(2, &b) }.not_to yield_control.at_least(3).at_most(4).times end From a518cdd1cf18b708c370078b4ef8116d0894108d Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 18 Mar 2020 20:32:05 +0000 Subject: [PATCH 3/4] Changelog for #1169 --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index d42b1e27e..d8bef4787 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,6 +17,8 @@ Enhancements: * Mocks expectations can now set a custom failure message. (Benoit Tigeot and Nicolas Zermati, #1156) * `aggregate_failures` now shows the backtrace line for each failure. (Fabricio Bedin, #1163) +* Support multiple combinations of `yield_control` modifiers like `at_least`, `at_most`. + (Jon Rowe, #1169) ### 3.9.0 / 2019-10-08 [Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.8.6...v3.9.0) From d4896a4d779411eb74b0aa2ed6415e6bbbba30ec Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Wed, 18 Mar 2020 21:22:17 +0000 Subject: [PATCH 4/4] 1.8.7 support for combined yield counts --- lib/rspec/matchers/built_in/yield.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/rspec/matchers/built_in/yield.rb b/lib/rspec/matchers/built_in/yield.rb index 818538e91..ecc87deb0 100644 --- a/lib/rspec/matchers/built_in/yield.rb +++ b/lib/rspec/matchers/built_in/yield.rb @@ -149,12 +149,22 @@ def times self end + if RUBY_VERSION.to_f > 1.8 + def cover?(count, number) + count.cover?(number) + end + else + def cover?(count, number) + number >= count.first && number <= count.last + end + end + # @private def matches?(block) @probe = YieldProbe.probe(block) return false unless @probe.has_block? return @probe.num_yields > 0 unless @expectation_type - return @expected_yields_count.cover?(@probe.num_yields) if @expectation_type == :<=> + return cover?(@expected_yields_count, @probe.num_yields) if @expectation_type == :<=> @probe.num_yields.__send__(@expectation_type, @expected_yields_count) end