Skip to content

Commit

Permalink
Fix Include Matcher For Ranges
Browse files Browse the repository at this point in the history
This is issue rspec#1191.

Previously, a few parts of the Include matcher assumed all Ranges
were iterable.  This caused it to raise errors like:
TypeError: Can't iterate from [Float|Time]

This happens because Ranges require that their beginning element implement
succ.  Float doesn't which causes the error.  Time is different because it
does implement succ but a) it's deprecated as of Ruby 1.9.2 and b) some Ruby
implementations raise an exception when trying to iterate through a range of
Time objects.

This PR does a few things:
1) Fixes the Include matcher to handle Ranges that don't support iteration (while
continuing to support Ranges that do)

This PR does a few things:
1) Fixes the Include matcher to handle Ranges that don't support iteration (while
continuing to support Ranges that do)
2) Adds specs for both types of Ranges in 1)  (There weren't any for the Include matcher
with Ranges previously)
  • Loading branch information
bclayman-sq committed Oct 4, 2021
1 parent dba6798 commit 29045c9
Show file tree
Hide file tree
Showing 2 changed files with 554 additions and 1 deletion.
15 changes: 15 additions & 0 deletions lib/rspec/matchers/built_in/include.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,34 @@ def actual_collection_includes?(expected_item)
# String lacks an `any?` method...
return false unless actual.respond_to?(:any?)

# Some objects don't support iteration (e.g. Float, Time, etc.)
return actual.include?(expected_item) if non_iterable_range?(actual)

actual.any? { |value| values_match?(expected_item, value) }
end

if RUBY_VERSION < '1.9'
def count_enumerable(expected_item)
return actual.include?(expected_item) ? 1 : 0 if non_iterable_range?(actual)
actual.select { |value| values_match?(expected_item, value) }.size
end
else
def count_enumerable(expected_item)
return actual.include?(expected_item) ? 1 : 0 if non_iterable_range?(actual)
actual.count { |value| values_match?(expected_item, value) }
end
end

# Determines if actual is a range and contains elements that do not support iteration.
# The usual requirement for a Range supporting iteration is that its beginning element
# implements `succ`. Time is different; it implements `succ` but that method is
# deprecated as of 1.9.2. Attempting to iterate through a Range of Times raises a
# TypeError on many common Ruby implementations. We treat Ranges of Time objects as
# non-iterable to ensure safety.
def non_iterable_range?(actual)
actual.is_a?(Range) && (!actual.min.respond_to?(:succ) || actual.min.is_a?(Time))
end

def count_inclusions
@divergent_items = expected
case actual
Expand Down
Loading

0 comments on commit 29045c9

Please sign in to comment.