Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spec helper it_iterates for iteration methods #10158

Merged

Conversation

straight-shoota
Copy link
Member

@straight-shoota straight-shoota commented Dec 30, 2020

It is a common idiom that iteration methods come in two flavours: One yielding a block, and one returning an iterator and both targeting different use cases.

Simple examle:

[1, 2, 3].each do |element|
  puts element
end

iter = [1, 2, 3].each
puts iter.next
puts iter.next
puts iter.next

The implementation is different. Every iterator is implemented as a separat type. But the core behaviour is identical: both flavours should iterate exactly the same lements.

This patch introduces a spec helper which makes it easy to test such methods very consicely and equally for both flavours:

it_iterates "Range#each", [1, 2, 3], (1..3).each
# ^helper   ^description  ^expected  ^call to method (without block)

The way it works is that it creates two examples which each call one flavour of the method and collects the iterated elements in an array. The result is compared to the expected value.
Because it needs to construct the method call to the yielding method, it needs to be a macro. I don't think there could be a reasonably useful implementation without a macro.

Specs with this helper are easier to read and write then doing it manually (the second commit contains many examples for that) and a single specification test both flavours.
This is very helpful to improve stdlib specs, but it's also generally useful for other libraries, so it is not just be a stdlib spec helper, but part of the spec module.

I already use this helper in #10130, but this is a proper proposal.

@asterite
Copy link
Member

asterite commented Jan 6, 2021

Was this already merged in a previous PR?

@straight-shoota
Copy link
Member Author

Yes #10130 already added it_iterates as an internal helper and #10203 uses it, too. This is a proposal to make it available in stdlib, so you can use it as a generic spec helper in shards. The implementation here is also more enhanced compared to the original one.

I've merged master and added a commit to remove the internal implementation.

@oprypin
Copy link
Member

oprypin commented Feb 23, 2021

See also: #4813 :D

@bcardiff
Copy link
Member

I like it as an internal helper for the std-lib specs, but I don't feel comfortable with it being a feature of the spec module in the std-lib. Specially if it is included by default.

@straight-shoota
Copy link
Member Author

I don't feel comfortable with it being a feature of the spec module in the std-lib

Why not? It's generally useful for testing any Iterator implementation, not just stdlib's. I'd like to have tools for such tests at hand for custom iterators.

It can certainly be an optional require, of course.

src/spec/helpers/iterate.cr Outdated Show resolved Hide resolved
#
# If the iteration elements are tuples (i.e. multiple values), the yielding
# variant by default only catches the first value because of the block argument
# mechanics. Passing `tuple: true` ensures all yielded arguments are collected
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tuple: true is a bit cryptic. Maybe something like:

yielded_args: :single (default)
yielded_args: :all

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminded me of this discussion
#10109 (comment)

But actually here I'd more likely support a boolean. It's a bit similar to reuse: true

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a boolean single_block_arg = true? Or tuple_block_arg = false?

@@ -0,0 +1,94 @@
module Spec::Methods
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Location-wise WDTY of "iterator/spec". I think that each module in the std-lib could use that as spec helpers. "log/spec" is already there.

Iterator is in the prelude so it might be special.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's probably a good idea in general. However, this helper is not just for Iterator but also Enumerable and in fact any iterating method. So I'd rather place it in a more generic location.

@straight-shoota
Copy link
Member Author

Bump. Having these advanced spec helpers available would be really nice.

Copy link
Member

@asterite asterite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's merge this! But CI is red.

Comment on lines 37 to 42
it "{{ description.id }} yielding" do
assert_iterates_yielding {{ expected }}, {{ method }}, infinite: {{ infinite }}, tuple: {{ tuple }}
end
it "{{ description.id }} iterator" do
assert_iterates_iterator {{ expected }}, {{ method }}, infinite: {{ infinite }}
end
Copy link
Member

@oprypin oprypin Jun 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, the description can't safely contain " and other special characters

Suggested change
it "{{ description.id }} yielding" do
assert_iterates_yielding {{ expected }}, {{ method }}, infinite: {{ infinite }}, tuple: {{ tuple }}
end
it "{{ description.id }} iterator" do
assert_iterates_iterator {{ expected }}, {{ method }}, infinite: {{ infinite }}
end
it {{ "#{description} yielding" }} do
assert_iterates_yielding {{ expected }}, {{ method }}, infinite: {{ infinite }}, tuple: {{ tuple }}
end
it {{ "#{description} iterator" }} do
assert_iterates_iterator {{ expected }}, {{ method }}, infinite: {{ infinite }}
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if marcos could be avoided at all for this feature.

Co-authored-by: Oleh Prypin <oleh@pryp.in>
@straight-shoota
Copy link
Member Author

@asterite (#10158 (comment)): Macros are necessary to call both, yielding and iterator, overloads from the same expression.

Simplified example:

it_iterates x.each # macro expands to `x.each { }` (yielding) and `x.each` (iterator)

This is impossible without macros.

If we avoid macros, we need to make both calls separate. I believe the API could receive the iterator as argument and the yielding variant as block argument:

it_iterates x.each do # iterator
  x.each { |item| yield item } # yielding
end

But: This is harder to understand, the block forwarding adds more complexity. We end up with a lot of boilerplate and duplication (which becomes even more a problem when the specced method is more complex than just x.each). Crystal has a great tool for fixing all of that: macros =)

@asterite asterite added this to the 1.1.0 milestone Jun 6, 2021
@asterite asterite merged commit a190f24 into crystal-lang:master Jun 6, 2021
@straight-shoota straight-shoota deleted the feature/spec-it_iterates branch June 7, 2021 08:16
@straight-shoota straight-shoota changed the title Add spec helper for iteration method Add spec helper for iteration methods Jun 29, 2021
@straight-shoota straight-shoota changed the title Add spec helper for iteration methods Add spec helper it_iterates for iteration methods Jun 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants