Skip to content

Add a TestConsole #4394

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

Merged
merged 5 commits into from
Jul 23, 2025
Merged

Conversation

morgen-peschke
Copy link
Contributor

Implements suggestion in #4393

Copy link
Member

@djspiewak djspiewak left a comment

Choose a reason for hiding this comment

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

Is there a reason to have a separate logsRef? It seems redundant given that we have stderr/stdout

*/
final class TestConsole[F[_]: Parallel](
stdInSemaphore: Semaphore[F],
stdInStateRef: Ref[F, TestStdInState[F]],
Copy link
Member

Choose a reason for hiding this comment

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

I'm kind of surprised at this structure and would have expected something more like a Queue. Wouldn't it be more useful to allow users to build tests which supply input more ergonomically?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand what specifically you're asking about.

If you're asking about the API, the users use TestConsole#write and TestConsole#writeln to supply input, they don't need to interact with the internal bits.

If you're asking about the structure of the internal bits, if this were implemented with a Queue, it would still need something like TestStdInState to track the partial lines, so it would only really get rid of the Semaphore.

Since I had to work with TestStdInState anyway, it was simpler to track the requests locally instead of delegating that to a Queue (especially since only one state needs that functionality anyway).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've separated out the algebras that control stdIn and inspect the state of things to hopefully clarify how the user would build tests and supply input.

@iRevive
Copy link
Contributor

iRevive commented May 22, 2025

Would it make sense to shape a test console around an ADT? We have this implementation in otel4s: https://github.com/typelevel/otel4s/blob/4b5f51093e3bb2fbb20914f901a5752a88d8e26c/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/test/InMemoryConsole.scala#L28

The ADT offers enough flexibility to test operations.

P. S. To be fair, it doesn't support read operations, but it can be implemented with a separate read queue.

@iRevive
Copy link
Contributor

iRevive commented May 22, 2025

On the other hand, since TestConsole covers different scenarios, it might be worth exploring two different implementations.

@morgen-peschke
Copy link
Contributor Author

Is there a reason to have a separate logsRef? It seems redundant given that we have stderr/stdout

Those are for capturing meta information that's useful when tests fail, like that there was X amount left in the buffer at close, the exact sequence of reads and writes, etc.

For context: I initially wrote this as a local helper when writing tests for typelevel/log4cats#912, and the additional context was really helpful for debugging those tests.

@morgen-peschke morgen-peschke requested a review from djspiewak May 26, 2025 17:42
@morgen-peschke
Copy link
Contributor Author

Would it make sense to shape a test console around an ADT?

I think the main reason I went with a design that provided the full output of each stream, rather than an ADT tracking each operation, is that it would encourage writing tests using it which would be less sensitive to internal implementation changes.

It feels like a smell if we encourage users to write tests that care if there's a switch under the hood from console.println(fooNel.mkString_("\n")) to fooNel.traverse_(console.println(_)), since the observable behavior is the same.

Copy link
Member

@djspiewak djspiewak left a comment

Choose a reason for hiding this comment

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

Sorry for taking so long to come back to this. I really like where this has ended up and I'd like to merge it in time for 3.7. Minor requested change on all the Show constraints and then we're ready to go I think

* @note
* Blocked calls will be woken in a first-in-first-out order.
*/
def write[A](value: A)(implicit S: Show[A]): F[Unit]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def write[A](value: A)(implicit S: Show[A]): F[Unit]
def write[A](value: A)(implicit S: Show[A] = Show.fromToString[A]): F[Unit]

We try to idiomatically follow this pattern (at least within CE) when dealing with Show, since universal toString is a better fallback default than compilation failure. All of implicit Shows here should be updated accordingly.

@djspiewak djspiewak added this to the v3.7.0 milestone Jul 23, 2025
@morgen-peschke morgen-peschke requested a review from djspiewak July 23, 2025 16:53
@djspiewak djspiewak merged commit 0801466 into typelevel:series/3.x Jul 23, 2025
34 of 38 checks passed
@morgen-peschke morgen-peschke deleted the test-console branch July 23, 2025 17:38
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.

3 participants