robstoll / atrium-roadmap

Contains issues created by the maintainers of Atrium

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

provide contains for array

robstoll opened this issue · comments

Since I got another request (robstoll/atrium#419) regarding

add support for contains for Array

I am opening the discussion if we want to support this.

So far people where happy enough with:

expect(arr).asList().contains....

But I understand that it is kind of cumbersome if one works a lot with arrays.

The problem with better support for array is that it would require a lot of duplication. There are 9 different Array types and everywhere we support Iterable we would need to add 9 additional duplicates. That's a lot of maintenance for little gain IMO.

I could imagine that we use a generative approach to tackle it so that we only have to write the Iterable version and derive the ones for the 9 different array types. However, still questionable if the effort and the maintenance afterwards is worth it.

Thoughts?

Is it an option to add an overloaded expect for every array type, that returns an appropriate List? As far as I can see, there is nothing specific about an array that we cannot check on a List. It would make the API easier to use and mean a manageable amount of maintenance effort.

You mean overloads which basically delegate to expect(arr).asList() right?
That would work, but only cover this particular case. It would not cover the following cases:

  • feature which return array -> one still has to use asList()
  • functions which expect iterable/collection/List one still has to conver the array with asIterable() or asList()

and I would prefer if things always need to be done the same way. Otherwise it seems a bit like an inconsistency that one can do expect(arr).contains(...) but not eg. expect(arrs).get(0).contains(...)

Hm, I see your point. Then generating the functions for any matcher that is defined for Expect<List<…>> would be the most maintainable solution. But I agree that it’s questionable whether it is worth the effort. Note that we would also need to make sure that third parties can use such a generator for their extensions.

On the other hand, this peculiarity of atrium might be something that shies people away in the very beginning, when picking their assertion library (“I am not going to use that, I can’t even test arrays!”).

On the other hand, this peculiarity of atrium might be something that shies people away in the very beginning, when picking their assertion library (“I am not going to use that, I can’t even test arrays!”).

Would be an argument to add at least overloads for expect(arr) but I don't know... is expect(arr).asList() really such a pain?

@dalewking what Array type do you usually deal with: Array<T> or one of the specializations IntArray etc.?

In the case I needed it was an Array, in fact I was trying to assertions on the values property of an enum

is expect(arr).asList() really such a pain?

I think using it is not a big deal once you know it. However, finding out that it is meant to be used like that might be. People just want to write down expect(myArr).hasLength(3) or expect(myArr).get(2).toBe(42) and are irritated why it is not possible. asList is not the obvious solution because, in that situation, people don’t want a list. They want to test an array.

That’s my take. Maybe @dalewking can talk about his experience.

On another note: would adding array overloads to all functions that deconstruct an Expect be that much of a maintenance burden? It clutters the code, that is true. But it would only be at the API level. And since the API is meant to be stable anyway, we would likely seldomly touch the functions again. So maybe it would be less of a maintenance burden (because there is no maintenance on them) but rather a mere annoyance?

is expect(arr).asList() really such a pain?

Believe you mean expect(arr.toList()). Is it a show stopper issue? No, and that is of course what I had to do. But when you try expect(arr).contains(...) and find that it is not supported would you feel that the fault of the test I am writing or that Atrium is missing a very basic feature?

I meant expect(arr).asList() or expect(arr).asIterable() in case you use a version < 0.9.0
This can be used at any place where your subject is an array to turn it into a list. E.g.:

expect(enum).feature { f(it::arr) }.asList().contains(....)

dalewking:
But when you try expect(arr).contains(...) and find that it is not supported would you feel that the fault of the test I am writing or that Atrium is missing a very basic feature?

I see your point and I am not against adding more support for arrays it should just be in a way which is manageable.

jGleitz:
Would adding array overloads to all functions that deconstruct an Expect be that much of a maintenance burden?

Yes, I think you really underestimate this. Have a look at arrayAssertions.kt it covers one functionality asList once without argument and once with an assertionCreator. We had to define 19 functions to support all Arrays. And the same will apply to most functions we defined for Iterable/List.
It's not like we don't add new functionality. Adding something to Iterable/List means again 9-11 more overloads to add for Array and if we are on it, why not also for Sequence. And it is not like we never change something in the API. A simple thing such as fixing a typo in KDoc suddenly needs to be done at ~10 places in addition.

Note further, that expect(arrayOf(1,2)).get(0) { ... } should return Expect<Array<Int>> and not Expect<List<Int>>. Which means we need to adopt the current builders which are based on Iterable or provide own builders for Array (which would be the better choice). There was some years back one contributor which started to work on this but eventually stopped because in his eyes it seemed not worth enough to go down this path and this was a contributor working a lot with arrays. He finally preferred the asIterable function over adding so much duplication.

Nevertheless, I support the idea of having better support for array. I would strive for a generative approach but would also be fine with the following compromise:

  • someone of you two create a good first issue for each feature we currently have (I guess will be something around 20 tasks) to duplicate the corresponding feature for the 9 array types.
  • someone of you two (or any other contributor) implements these tasks
  • someone of you two will pre-review the corresponding PRs
  • if there are maintenance tasks to do (such as update KDoc) then I'll ping one of you two and you do the necessary changes in a timely manner.
  • we will come back to the generative approach once you two are sick of the search&replace, copy&paste maintenance tasks

How does that sound to you? If you don't like the compromise then I'll create an issue for the generative approach instead.

Thanks for the elaborate explanation.

I want to clarify before I make a decision on whether I want to put up with this: I was still thinking of making deconstructing functions support arrays. E.g. add overloads like

fun expect(subject: Array) = expect(subject).asList()

and

Expect<List<Array<*>>.get(index: Int) = get(index).asList()

and similarily for the feature functions, etc. However, your explanation still seems to talk about copying List matchers.

From my point of view, my proposal has some advantages: When copying the List matchers to add Array versions, we need to cover almost all of them for the change to be useful. On the other hand, I expect that we already cover 80% of use cases if we just add the expect overload I propose.

Note further, that expect(arrayOf(1,2)).get(0) { ... } should return Expect<Array<Int>> and not Expect<List<Int>>.

With my approach, I think it would rightfully return an Expect<List>. Because we would try to eliminate all Expect<Array> versions.

What are your thoughts on the advantages and disadvantages of the two approaches?

and if we are on it, why not also for Sequence

I agree, if we tackle arrays, we should also support Sequence.

How does that sound to you?

Bowing out of this conversation as I have had to abandon Atrium due to lack of Kotlin Native iOS support

@dalewking fair enough, thanks for your input so far, appreciated 👍

@jGleitz if I get you right, you would like to turn Arrays into Lists for all cases where a function could potentially return an array. I have a very bad feeling with this because:

  • it could be it requires even more duplication as you need to add special overloads for the case where a type parameter is an Array
  • we cannot force a user to do this and most likely a user won't, so back to the initial problem that the user has suddenly an Array at hand.

Also, if I were using array a lot then it is likely that I would create extension-methods for it.
if we turn array into a list, then I could not use it in tests and would need to duplicate it on List.

if I get you right, you would like to turn Arrays into Lists for all cases where a function could potentially return an array.

Yes, that is my idea.

it could be it requires even more duplication as you need to add special overloads for the case where a type parameter is an Array

I am not sure what you mean by that. From my understanding, we would need array overloads for every function that deconstructs an Expect. I don’t know whether those are more or less, but I would intuitively expect that there are less.

we cannot force a user to do this and most likely a user won't, so back to the initial problem that the user has suddenly an Array at hand.

I am also not sure what you mean by that. Somebody who is just using the atrium API would automatically get an Expect<List> whenever they would normally get an Expect<Array>. If they use custom matchers which are not based on atrium’s existing matchers and hence do not convert, then users would be forced to call toList() themselves, that is true. But I expect that those cases will be very rare. And the remedy is still not that bad.

Also, if I were using array a lot then it is likely that I would create extension-methods for it.
if we turn array into a list, then I could not use it in tests and would need to duplicate it on List.

That is a good point. I would need to see some use cases to judge how bad that is, though. Because where would I use those functions? Maybe we can adapt the feature functions to deal with that? Maybe I use the functions before handing the values to atrium? As far as I can see, it would only be a problem if I use the functions on a value after I have passed it to expect, e.g. in feature matchers.

above I guessed that we have around 20 functions to transform for Array.
We have alone 18 feature overloads + 14 MetaFeatureOption + 10 MetaFeatureBuilder
This would not be an issue if we use a generative approach but would be if we do it manually IMO.

What I meant with

you need to add special overloads for the case where a type parameter is an Array

Every time you write an own assertion function for a type which has type parameters where you want to extract this type you would need to add the 9 to 11 overloads for array. For instance:
Expect<Result<T>>.isSuccess

With your proposition we put the maintenance burden not only on us but also on potential Atrium extension writers. Say one writes an extension for Arrow which has a lot of data structures with type parameters (and I guess this will happen at one point). Do you see what I mean?

If we duplicate the Iterable/List functions on the other hand, then we have:

  • clean functions for Array which return the same type; which means Array will also be a first type citizen in Atrium - a user never has to call asList()
  • the maintenance burden is only on us, assuming the generator we use is re-usable for others -- I would assume that it could be re-usable also for other projects not only in the context of Atrium, other libraries certainly have the same problem. That being said, maybe there is already a generator (Jetbrains has one to generate their Array functions but I don't know if it is open source)
  • we don't require overloads for expect
  • extension methods for Array (as any other existing method for array types) can be used in feature

Hm. It sounded all well in my head. But you are right, duplicating the assertions is the better approach.

Alright, the remaining question is how we want to proceed: create a task for a generative approach or are you willing to do the work as suggested hear:
#75 (comment)

I’ve done some code generations with kotlinpoet and it was fairly easy. Especially because building Gradle tasks is so easy. So if I was to pick it up (which I plan to, but I can’t promise), I would probably do that.

good, I will create a task and whoever wants to work on it can (I'll leave it open to the implementer what generative approach is taken)

Created, robstoll/atrium#459 closing this