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

[xef-evaluator] - Improvements #568

Closed
wants to merge 10 commits into from
Closed

Conversation

tomascayuelas
Copy link
Contributor

@tomascayuelas tomascayuelas commented Nov 29, 2023

Improvements:

  • Content validation
  • Richer domain
  • Change DSL
  • Adding tests

Don't hesitate to comment anything.

javipacheco
javipacheco previously approved these changes Nov 30, 2023
Copy link
Contributor

@javipacheco javipacheco left a 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.

Copy link
Contributor

@fedefernandez fedefernandez left a 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.

fedefernandez
fedefernandez previously approved these changes Nov 30, 2023
@tomascayuelas
Copy link
Contributor Author

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.

@fedefernandez
Copy link
Contributor

@tomascayuelas I made a migration in this commit, in case it helps
2c5a852
Arrow's closed related PRs are also a good source of info

@tomascayuelas
Copy link
Contributor Author

@tomascayuelas I made a migration in this commit, in case it helps

2c5a852

Arrow's closed related PRs are also a good source of info

Ohhh perfect! Thanks @fedefernandez

Copy link
Contributor

@fedefernandez fedefernandez left a 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 {
Copy link
Contributor

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

Copy link
Contributor

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/build.gradle.kts Show resolved Hide resolved
class EitherOpsSpec {
private val validSuiteSpec = runBlocking {
SuiteSpec(simpleString.next()) {
outputDescription { simpleString.next() }
Copy link
Contributor

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.

Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Contributor

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))
Copy link
Contributor

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+")
Copy link
Contributor

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 {
Copy link
Contributor

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

@tomascayuelas
Copy link
Contributor Author

Thank you very much @fedefernandez for your comments/review.
As we have already spoken by slack, I take care of them, I think they are very good, and can be better code. Thank you!

@raulraja
Copy link
Contributor

raulraja commented Mar 5, 2024

@fedefernandez @tomascayuelas is this still relevant?

Copy link
Contributor

@nomisRev nomisRev left a 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 {
Copy link
Contributor

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.

Comment on lines +19 to +23
suspend fun contextDescription(block: suspend () -> String) =
contexts.add(ContextDescription.invoke { block() })

suspend fun outputResponse(block: suspend () -> String) =
outputs.add(OutputResponse.invoke { block() })
Copy link
Contributor

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()
Copy link
Contributor

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?

Comment on lines +12 to +13
suspend operator fun invoke(
block: suspend () -> String
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Contributor

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) {
Copy link
Contributor

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() }
Copy link
Contributor

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 {
Copy link
Contributor

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.

@fedefernandez
Copy link
Contributor

fedefernandez commented Mar 5, 2024

@fedefernandez @tomascayuelas is this still relevant?

@raulraja I feel it's. I can take a look and prepare for reviewing

We're here using Kotest inside Kotlin Test 🤕 Never seen that before 😅 Why are we using both?

@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.

arrow-kt/arrow#3183

@nomisRev
Copy link
Contributor

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.

@nomisRev nomisRev self-requested a review March 13, 2024 07:48
@fedefernandez
Copy link
Contributor

Thanks @nomisRev. I have already started resolving some comments; I'll get back to you once I have something.

@fedefernandez
Copy link
Contributor

I'm afraid I don't have enough context to fix the conflicts. Therefore, I vote to close the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants