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

Print methods of the Console effect should not depend on Abort[IOException] #1069

Merged
merged 5 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 62 additions & 27 deletions kyo-core/shared/src/main/scala/kyo/Console.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import java.io.EOFException
import java.io.IOException

/** Represents a console for input and output operations.
*
* The methods that print to the console output and error streams ([[print]], [[printErr]], [[printLine]], [[printLineErr]]) don't return
* an [[Abort]] effect because they don't throw exceptions. The behavior is the same as the standard Scala `scala.Console` methods, which
* don't throw exceptions either. The cause is the underlying Java `PrintStream` class implementation, which doesn't throw exceptions when
* writing to the console output or error streams (see
* [[https://stackoverflow.com/questions/297303/printwriter-and-printstream-never-throw-ioexceptions PrintWriter and PrintStream never throw IOExceptions]]
* for more details).
*
* To check if an error occurred in the console output or error streams, use the [[checkErrors]], which returns a boolean indicating if an
* error occurred.
*/
final case class Console(unsafe: Console.Unsafe):

Expand All @@ -19,34 +29,41 @@ final case class Console(unsafe: Console.Unsafe):
* @param s
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you also update the top Console scaladoc with the rationale for not having a pending abort + the role of checkErrors? I think it's important to clearly flag this behavior to the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Let me know if the text works for you.

Copy link
Collaborator

Choose a reason for hiding this comment

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

perfect! thanks

* The string to print.
*/
def print(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.print(s.show)))
def print(s: Text)(using Frame): Unit < IO = IO.Unsafe(unsafe.print(s.show))

/** Prints a string to the console's error stream without a newline.
*
* @param s
* The string to print to the error stream.
*/
def printErr(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printErr(s.show)))
def printErr(s: Text)(using Frame): Unit < IO = IO.Unsafe(unsafe.printErr(s.show))

/** Prints a string to the console followed by a newline.
*
* @param s
* The string to print.
*/
def println(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printLine(s.show)))
def println(s: Text)(using Frame): Unit < IO = IO.Unsafe(unsafe.printLine(s.show))

/** Prints a string to the console's error stream followed by a newline.
*
* @param s
* The string to print to the error stream.
*/
def printLineErr(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printLineErr(s.show)))
def printLineErr(s: Text)(using Frame): Unit < IO = IO.Unsafe(unsafe.printLineErr(s.show))

/** Checks if an error occurred in the console output or error streams.
*
* @return
* True if an error occurred, false otherwise.
*/
def checkErrors(using Frame): Boolean < IO = IO.Unsafe(unsafe.checkErrors)

/** Flushes the console output streams.
*
* This method ensures that any buffered output is written to the console.
*/
def flush(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.flush()))
def flush(using Frame): Unit < IO = IO.Unsafe(unsafe.flush())
end Console

/** Companion object for Console, providing utility methods and a live implementation.
Expand All @@ -60,11 +77,12 @@ object Console:
def readLine()(using AllowUnsafe) =
Result.catching[IOException](Maybe(scala.Console.in.readLine()))
.flatMap(_.toResult(Result.fail(new EOFException("Consoles.readLine failed."))))
def print(s: String)(using AllowUnsafe) = Result.catching[IOException](scala.Console.out.print(s))
def printErr(s: String)(using AllowUnsafe) = Result.catching[IOException](scala.Console.err.print(s))
def printLine(s: String)(using AllowUnsafe) = Result.catching[IOException](scala.Console.out.println(s))
def printLineErr(s: String)(using AllowUnsafe) = Result.catching[IOException](scala.Console.err.println(s))
def flush()(using AllowUnsafe) = Result.catching[IOException](scala.Console.flush())
def print(s: String)(using AllowUnsafe) = scala.Console.out.print(s)
def printErr(s: String)(using AllowUnsafe) = scala.Console.err.print(s)
def printLine(s: String)(using AllowUnsafe) = scala.Console.out.println(s)
def printLineErr(s: String)(using AllowUnsafe) = scala.Console.err.println(s)
def checkErrors(using AllowUnsafe): Boolean = scala.Console.out.checkError() || scala.Console.err.checkError()
def flush()(using AllowUnsafe) = scala.Console.flush()
)

private val local = Local.init(live)
Expand Down Expand Up @@ -153,10 +171,18 @@ object Console:
val stdErr = new StringBuffer
val proxy =
new Proxy(console.unsafe):
override def print(s: String)(using AllowUnsafe) = Result.succeed(stdOut.append(s)).unit
override def printErr(s: String)(using AllowUnsafe) = Result.succeed(stdErr.append(s)).unit
override def printLine(s: String)(using AllowUnsafe) = Result.succeed(stdOut.append(s + "\n")).unit
override def printLineErr(s: String)(using AllowUnsafe) = Result.succeed(stdErr.append(s + "\n")).unit
override def print(s: String)(using AllowUnsafe) =
stdOut.append(s)
()
override def printErr(s: String)(using AllowUnsafe) =
stdErr.append(s)
()
override def printLine(s: String)(using AllowUnsafe) =
stdOut.append(s + "\n")
()
override def printLineErr(s: String)(using AllowUnsafe) =
stdErr.append(s + "\n")
()
let(Console(proxy))(v)
.map(r => IO((Out(stdOut.toString(), stdErr.toString()), r)))
}
Expand All @@ -181,41 +207,49 @@ object Console:
* @param v
* The value to print.
*/
def print[A](v: A)(using Frame): Unit < (IO & Abort[IOException]) =
IO.Unsafe.withLocal(local)(console => Abort.get(console.unsafe.print(toString(v))))
def print[A](v: A)(using Frame): Unit < IO =
IO.Unsafe.withLocal(local)(console => console.unsafe.print(toString(v)))

/** Prints a value to the console's error stream without a newline.
*
* @param v
* The value to print to the error stream.
*/
def printErr[A](v: A)(using Frame): Unit < (IO & Abort[IOException]) =
IO.Unsafe.withLocal(local)(console => Abort.get(console.unsafe.printErr(toString(v))))
def printErr[A](v: A)(using Frame): Unit < IO =
IO.Unsafe.withLocal(local)(console => console.unsafe.printErr(toString(v)))

/** Prints a value to the console followed by a newline.
*
* @param v
* The value to print.
*/
def printLine[A](v: A)(using Frame): Unit < (IO & Abort[IOException]) =
IO.Unsafe.withLocal(local)(console => Abort.get(console.unsafe.printLine(toString(v))))
def printLine[A](v: A)(using Frame): Unit < IO =
IO.Unsafe.withLocal(local)(console => console.unsafe.printLine(toString(v)))

/** Prints a value to the console's error stream followed by a newline.
*
* @param v
* The value to print to the error stream.
*/
def printLineErr[A](v: A)(using Frame): Unit < (IO & Abort[IOException]) =
IO.Unsafe.withLocal(local)(console => Abort.get(console.unsafe.printLineErr(toString(v))))
def printLineErr[A](v: A)(using Frame): Unit < IO =
IO.Unsafe.withLocal(local)(console => console.unsafe.printLineErr(toString(v)))

/** Checks if an error occurred in the console output or error streams.
*
* @return
* True if an error occurred, false otherwise.
*/
def checkErrors(using Frame): Boolean < IO = IO.Unsafe.withLocal(local)(console => console.unsafe.checkErrors)

/** WARNING: Low-level API meant for integrations, libraries, and performance-sensitive code. See AllowUnsafe for more details. */
abstract class Unsafe:
def readLine()(using AllowUnsafe): Result[IOException, String]
def print(s: String)(using AllowUnsafe): Result[IOException, Unit]
def printErr(s: String)(using AllowUnsafe): Result[IOException, Unit]
def printLine(s: String)(using AllowUnsafe): Result[IOException, Unit]
def printLineErr(s: String)(using AllowUnsafe): Result[IOException, Unit]
def flush()(using AllowUnsafe): Result[IOException, Unit]
def print(s: String)(using AllowUnsafe): Unit
def printErr(s: String)(using AllowUnsafe): Unit
def printLine(s: String)(using AllowUnsafe): Unit
def printLineErr(s: String)(using AllowUnsafe): Unit
def checkErrors(using AllowUnsafe): Boolean
def flush()(using AllowUnsafe): Unit
def safe: Console = Console(this)
end Unsafe

Expand All @@ -225,6 +259,7 @@ object Console:
def printErr(s: String)(using AllowUnsafe) = underlying.printErr(s)
def printLine(s: String)(using AllowUnsafe) = underlying.printLine(s)
def printLineErr(s: String)(using AllowUnsafe) = underlying.printLineErr(s)
def checkErrors(using AllowUnsafe): Boolean = underlying.checkErrors
def flush()(using AllowUnsafe) = underlying.flush()
end Proxy

Expand Down
58 changes: 52 additions & 6 deletions kyo-core/shared/src/test/scala/kyo/ConsoleTest.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package kyo

import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.OutputStream
import java.io.PrintStream
import java.io.StringWriter

class ConsoleTest extends Test:

case class Obj(a: String)
Expand Down Expand Up @@ -34,6 +40,40 @@ class ConsoleTest extends Test:
}
}

"checkErrors on out channel" in {
val buffer = new PrintStream(new OutputStream:
override def write(b: Int): Unit = throw IOException())
scala.Console.withOut(buffer) {
import AllowUnsafe.embrace.danger
val (r1, r2) =
IO.Unsafe.evalOrThrow {
for
r1 <- Abort.run(Console.print("test"))
r2 <- Abort.run(Console.checkErrors)
yield (r1, r2)
}
assert(r1.isSuccess && r2.isSuccess)
assert(r2.getOrThrow)
}
}

"checkErrors on err channel" in {
val buffer = new PrintStream(new OutputStream:
override def write(b: Int): Unit = throw IOException())
scala.Console.withErr(buffer) {
import AllowUnsafe.embrace.danger
val (r1, r2) =
IO.Unsafe.evalOrThrow {
for
r1 <- Abort.run(Console.printErr("test"))
r2 <- Abort.run(Console.checkErrors)
yield (r1, r2)
}
assert(r1.isSuccess && r2.isSuccess)
assert(r2.getOrThrow)
}
}

"live" in {
val output = new StringBuilder
val error = new StringBuilder
Expand Down Expand Up @@ -90,14 +130,19 @@ class ConsoleTest extends Test:
assert(testUnsafe.printlnErrs.head == "test error line")
}

"should check errors correctly" in {
val testUnsafe = new TestUnsafeConsole(error = true)
assert(testUnsafe.checkErrors)
}

"should convert to safe Console" in {
val testUnsafe = new TestUnsafeConsole()
val safeConsole = testUnsafe.safe
assert(safeConsole.isInstanceOf[Console])
}
}

class TestUnsafeConsole(input: String = "") extends Console.Unsafe:
class TestUnsafeConsole(input: String = "", error: Boolean = false) extends Console.Unsafe:
var readlnInput = input
var prints = List.empty[String]
var printErrs = List.empty[String]
Expand All @@ -108,17 +153,18 @@ class ConsoleTest extends Test:
Result.succeed(readlnInput)
def print(s: String)(using AllowUnsafe) =
prints = s :: prints
Result.unit
()
def printErr(s: String)(using AllowUnsafe) =
printErrs = s :: printErrs
Result.unit
()
def printLine(s: String)(using AllowUnsafe) =
printlns = s :: printlns
Result.unit
()
def printLineErr(s: String)(using AllowUnsafe) =
printlnErrs = s :: printlnErrs
Result.unit
def flush()(using AllowUnsafe) = Result.unit
()
def checkErrors(using AllowUnsafe): Boolean = error
def flush()(using AllowUnsafe) = ()
end TestUnsafeConsole

end ConsoleTest
Loading