-
Notifications
You must be signed in to change notification settings - Fork 15
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
[xef-evaluator] - Improvements #568
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Great job!!
I have tested the evaluator locally and everything works as expected.
evaluator-example/src/main/kotlin/com/xebia/functional/xef/evaluator/examples/TestExample.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great @tomascayuelas. Just one comment, how hard would be to use the kotlin.test
framework instead kotest
? In Arrow they followed the approach of using kotest just for the assertions due to compatibility reasons with the platform.
Thanks @fedefernandez I'll check it and see if it's simple. |
@tomascayuelas I made a migration in this commit, in case it helps |
Ohhh perfect! Thanks @fedefernandez |
Adding test for EitherOps functions
33fd96c
57ff093
to
33fd96c
Compare
bed6f58
to
a763d4d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some minor comments. I think it would be worthy to address or discuss
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
|
||
object EitherOps { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question, in Kotlin is this a good practice (nested the functions in an object)? I have seen in the project the extension functions defined at top level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's a pretty bad practice sadly. It doesn't play well with auto-complete for imports, in Kotlin.
These should just be top-level function, when we get proper namespaces in Kotlin 2.x this will probably improve but for now they need to be top-level.
evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/errors/SuiteSpecError.kt
Show resolved
Hide resolved
class EitherOpsSpec { | ||
private val validSuiteSpec = runBlocking { | ||
SuiteSpec(simpleString.next()) { | ||
outputDescription { simpleString.next() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each time this test is executed, it runs once with random data, right? In that case, I think it would be better to use fixed values because, right now, can lead to flaky tests. Not a big deal though, just a preference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we're using Kotest here, we can potentially piggy back on their SEED
which is random and printed on failure.
Otherwise I would at least work with a SEED, and print it somewhere on each build, or use fixed data like @fedefernandez mentioned. I am not a fan of next()
at all, since it's quite unpredictable.
} | ||
|
||
@Test | ||
fun shouldInvalidWhenAnyCombinationOfInputAreEmpty() = runTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the invalid tests could be replaced by a property test that generates a tuple of the invalid ItemSpec
+ the error list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we look into property testing? It's supported by Kotest out-of-the-box, and has some cool stuff like Exhaustive
. In contrast to Arb
it only defines "edge cases", and makes sure that by minimum the full cartesian product of all edge cases are run by the tests.
That's pretty cool, since it can be used to run the least amount of tests but still cover all edge cases + any configurable amount of random values.
outputResponse { emptyOutputResponse } | ||
} | ||
} | ||
.shouldBeLeft() shouldBe listOf<ValidationError>(EmptyItemSpecOutputResponse(1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. I prefer the way ItemSpecBuilderSpec
defines the data, but here we can also group some tests in a single one.
|
||
object Generators { | ||
val simpleString = Arb.string(1, 60, Codepoint.az()) | ||
val emptyString = Arb.stringPattern("\\s+") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could be probably more efficient to generate a random-size list of one of the blank spaces
import kotlin.test.Test | ||
import kotlinx.coroutines.test.runTest | ||
|
||
class TestModelsSpec { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These can me coded as a property-based tests
Thank you very much @fedefernandez for your comments/review. |
@fedefernandez @tomascayuelas is this still relevant? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're here using Kotest inside Kotlin Test 🤕 Never seen that before 😅 Why are we using both?
We can also define a simple DSL by ourselves that covers the requirements of this PR. It's probably not much more than what we've already defined on top of Kotest.
Additionally, I agree with @fedefernandez that we should either properly do prop testing (with random/configurable SEED), or use fixed data.
A couple small questions, thoughts, and concerns on some the domain classes defined in this PR.
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
|
||
object EitherOps { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's a pretty bad practice sadly. It doesn't play well with auto-complete for imports, in Kotlin.
These should just be top-level function, when we get proper namespaces in Kotlin 2.x this will probably improve but for now they need to be top-level.
suspend fun contextDescription(block: suspend () -> String) = | ||
contexts.add(ContextDescription.invoke { block() }) | ||
|
||
suspend fun outputResponse(block: suspend () -> String) = | ||
outputs.add(OutputResponse.invoke { block() }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these functions ever called in parallel? If yes then MutableList
will result in race conditions here. Use Atomic<List<_>>
to fix.
{ ensure(input.isNotBlank()) { EmptyItemSpecInput } }, | ||
{ | ||
mapOrAccumulate(contexts.withIndex()) { nameAndIndex -> | ||
nameAndIndex.value.recover { error -> raise(error) }.bind() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is .recover { error -> raise(error) }
doing here?
suspend operator fun invoke( | ||
block: suspend () -> String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suspend operator fun invoke( | |
block: suspend () -> String | |
inline operator fun invoke( | |
block: () -> String |
Why is this suspend
?
@JvmInline | ||
value class ContextDescription(val value: String) { | ||
companion object { | ||
@JvmSynthetic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is @JvmSynthetic
being added here?
import com.xebia.functional.xef.evaluator.models.errors.EmptyContextDescription | ||
|
||
@JvmInline | ||
value class ContextDescription(val value: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this constructor be private to prevent ContextDescription
to be created with an empty string?
Is this because it needs to be parsed from JSON using reflection? 😭
class EitherOpsSpec { | ||
private val validSuiteSpec = runBlocking { | ||
SuiteSpec(simpleString.next()) { | ||
outputDescription { simpleString.next() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we're using Kotest here, we can potentially piggy back on their SEED
which is random and printed on failure.
Otherwise I would at least work with a SEED, and print it somewhere on each build, or use fixed data like @fedefernandez mentioned. I am not a fan of next()
at all, since it's quite unpredictable.
} | ||
|
||
@Test | ||
fun shouldInvalidWhenAnyCombinationOfInputAreEmpty() = runTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we look into property testing? It's supported by Kotest out-of-the-box, and has some cool stuff like Exhaustive
. In contrast to Arb
it only defines "edge cases", and makes sure that by minimum the full cartesian product of all edge cases are run by the tests.
That's pretty cool, since it can be used to run the least amount of tests but still cover all edge cases + any configurable amount of random values.
@raulraja I feel it's. I can take a look and prepare for reviewing
@nomisRev We faced some binary incompatibilities and found the issues you created in Arrow. We're using the kotlin test framework but kotest for the assertions. |
I see I am blocking this PR @fedefernandez. To give some context on Arrow, that was to avoid rewriting more code and to political continue supporting Kotest. We replaced the Kotest runner only because we wanted to support WASM, and be able to build EAP releases (as part of the community builds). However, I don't want to block this PR on that matter. I do feel we can simplify some things here, Junit also support "property based testing" in the form of table based testing. Not sure if Qwik works for KMP, but I've used it for kotlin-test on JVM. That leaves the same concerns as you originally raised @fedefernandez concerning testing practices. I am removing my request for changes however to unblock this PR from my side. |
Thanks @nomisRev. I have already started resolving some comments; I'll get back to you once I have something. |
I'm afraid I don't have enough context to fix the conflicts. Therefore, I vote to close the PR. |
Improvements:
Don't hesitate to comment anything.