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

Kyo test #1081

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
20 changes: 19 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ val compilerOptions = Set(
ScalacOptions.deprecation,
ScalacOptions.warnValueDiscard,
ScalacOptions.warnNonUnitStatement,
ScalacOptions.languageStrictEquality,
ScalacOptions.other("-explain-cyclic"),
// ScalacOptions.languageStrictEquality,
ScalacOptions.release("11"),
ScalacOptions.advancedKindProjector
)
Expand Down Expand Up @@ -485,6 +486,23 @@ lazy val `kyo-caliban` =
)
.jvmSettings(mimaCheck(false))

lazy val `kyo-test` =
crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Full)
.in(file("kyo-test"))
.dependsOn(`kyo-core`)
.dependsOn(`kyo-combinators`)
.dependsOn(`kyo-zio`)
.dependsOn(`kyo-stm`)
.settings(
`kyo-settings`,
)
.jsSettings(
`js-settings`
)
.jvmSettings(mimaCheck(false))

lazy val `kyo-zio-test` =
crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
Expand Down
5 changes: 4 additions & 1 deletion kyo-core/jvm/src/main/scala/kyo/Process.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package kyo

import java.io.*
import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.Process as JProcess
import java.lang.ProcessBuilder.Redirect
import java.lang.System as JSystem
Expand Down
24 changes: 24 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/Ansi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ import scala.util.control.NonFatal
/** Provides ANSI color and formatting utilities for strings.
*/
object Ansi:
sealed abstract class AnsiCode(val code: String)
sealed abstract class Color(code: String) extends AnsiCode(code)

object Color:
case object Blue extends Color("\u001b[34m")
case object Cyan extends Color("\u001b[36m")
case object Green extends Color("\u001b[32m")
case object Magenta extends Color("\u001b[35m")
case object Red extends Color("\u001b[31m")
case object Yellow extends Color("\u001b[33m")
end Color

sealed abstract class Style(code: String) extends AnsiCode(code)

object Style:
case object Bold extends Style("\u001b[1m")
case object Faint extends Style("\u001b[2m")
case object Underlined extends Style("\u001b[4m")
case object Reversed extends Style("\u001b[7m")
end Style

extension (str: String)
/** Applies black color to the string. */
Expand Down Expand Up @@ -45,9 +65,13 @@ object Ansi:

/** Applies underline formatting to the string. */
def underline: String = s"\u001b[4m$str\u001b[0m"
def faint: String = withAnsi(Style.Faint)
def inverted: String = withAnsi(Style.Reversed)

/** Removes all ANSI escape sequences from the string. */
def stripAnsi: String = str.replaceAll("\u001b\\[[0-9;]*[a-zA-Z]", "")

def withAnsi(code: AnsiCode): String = s"${code.code}$str\u001b[0m"
end extension

object highlight:
Expand Down
11 changes: 11 additions & 0 deletions kyo-prelude/shared/src/main/scala/kyo/Stream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,17 @@ object Stream:
}
}

/** Creates a stream by unfolding a seed value as long as f returns Some value. */
def unfold[V, T, S](z: T)(f: T => Option[(V, T)])(using tag: Tag[Emit[Chunk[V]]], frame: Frame): Stream[V, S] =
f(z) match
case Some((v, next)) =>
Stream {
Emit.valueWith(Chunk.from(Seq(v))) {
unfold(next)(f).emit
}
}
case None => Stream.empty

/** A dummy type that can be used as implicit evidence to help the compiler discriminate between overloaded methods.
*/
sealed class Dummy
Expand Down
375 changes: 375 additions & 0 deletions kyo-test/TODO.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kyo.test

import kyo.*

private[test] trait TestClockPlatformSpecific:
self: TestClock.Test =>

def scheduler(implicit trace: Trace): Scheduler < Async =
Kyo.runtime[Any].map { runtime =>
new Scheduler:
def schedule(runnable: Runnable, duration: Duration)(implicit unsafe: Unsafe): Scheduler.CancelToken =
val fiber =
runtime.unsafe.fork(Kyo.sleep(duration) *> Kyo.pure(runnable.run()))
() => runtime.unsafe.run(fiber.interrupt).getOrThrowFiberFailure().isInterrupted
end schedule
}
end TestClockPlatformSpecific
30 changes: 30 additions & 0 deletions kyo-test/js/src/main/scala/kyo/test/TestDebug.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kyo.test

// TODO Implement this with appropriate JS filesystem APIs after JVM version is finalized
private[test] object TestDebug:
def print(executionEvent: ExecutionEvent, lock: TestDebugFileLock): Unit < Any =
executionEvent match
case t: ExecutionEvent.TestStarted =>
write(t.fullyQualifiedName, s"${t.labels.mkString(" - ")} STARTED\n", true, lock)

case t: ExecutionEvent.Test[?] =>
removeLine(t.fullyQualifiedName, t.labels.mkString(" - ") + " STARTED", lock)

case _ => ()

private def write(
fullyQualifiedTaskName: String,
content: => String,
append: Boolean,
lock: TestDebugFileLock
): Unit < Any =
()

private def removeLine(fullyQualifiedTaskName: String, searchString: String, lock: TestDebugFileLock): Unit < Any =
()

def createDebugFile(fullyQualifiedTaskName: String): Unit < Any =
()

def deleteIfEmpty(fullyQualifiedTaskName: String): Unit < Any = ()
end TestDebug
18 changes: 18 additions & 0 deletions kyo-test/js/src/main/scala/kyo/test/TestPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kyo.test

/** `TestPlatform` provides information about the platform tests are being run on to enable platform specific test configuration.
*/
object TestPlatform:

/** Returns whether the current platform is ScalaJS.
*/
final val isJS = true

/** Returns whether the currently platform is the JVM.
*/
final val isJVM = false

/** Returns whether the current platform is Scala Native.
*/
final val isNative = false
end TestPlatform
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kyo.test.results

import java.io.IOException
import kyo.*

private[test] trait ResultFileOps:
def write(content: => String, append: Boolean): Unit < (Env[Any] & Abort[IOException])

private[test] object ResultFileOps:
val live: Layer[ResultFileOps, Any] =
Layer(
Json()
)

private[test] case class Json() extends ResultFileOps:
def write(content: => String, append: Boolean): Unit < (Env[Any] & Abort[IOException]) =
()
end ResultFileOps
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kyo.test.results

import kyo.*
import kyo.test.*

private[test] object ResultPrinterJson:
val live: Layer[ResultPrinter, Any] =
Layer.init(
ResultSerializer.live,
ResultFileOps.live,
Layer.from((serializer, resultFileOps) => LiveImpl(serializer, resultFileOps))
)

private case class LiveImpl(serializer: ResultSerializer, resultFileOps: ResultFileOps) extends ResultPrinter:
override def print[E](event: ExecutionEvent.Test[E]): Unit < (Env[Any] & Abort[Nothing]) =
resultFileOps.write(serializer.render(event), append = true).orPanic
end ResultPrinterJson
87 changes: 87 additions & 0 deletions kyo-test/jvm/src/main/scala/kyo/test/TestDebug.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package kyo.test

import java.io.File
import java.io.PrintWriter
import java.nio.file.Files
import java.nio.file.Paths
import kyo.*
import scala.io.Source

private[test] object TestDebug:
private val outputDirectory = "target/test-reports-zio"
private def outputFileForTask(task: String) = s"$outputDirectory/${task}_debug.txt"
private val tasks = Platform.newConcurrentSet[String]()(Unsafe.unsafe)

private def createDebugFile(fullyQualifiedTaskName: String): Unit < Any = Kyo.pure {
if tasks.add(fullyQualifiedTaskName) then
makeOutputDirectory()
val file = new File(outputFileForTask(fullyQualifiedTaskName))
if file.exists() then file.delete()
file.createNewFile()
}

private def makeOutputDirectory(): Unit =
val fp = Paths.get(outputDirectory)
Files.createDirectories(fp.getParent)

def deleteIfEmpty(fullyQualifiedTaskName: String): Unit < Any = Kyo.pure {
if tasks.remove(fullyQualifiedTaskName) then
val file = new File(outputFileForTask(fullyQualifiedTaskName))
if file.exists() then
val source = Source.fromFile(file)
val nonBlankLines = source.getLines.filterNot(isBlank).toList
source.close()
if nonBlankLines.isEmpty then
file.delete()
end if
}

private def isBlank(input: String): Boolean =
input.toCharArray.forall(Character.isWhitespace(_))

def print(executionEvent: ExecutionEvent, lock: TestDebugFileLock): Unit < Any =
executionEvent match
case t: ExecutionEvent.TestStarted =>
createDebugFile(t.fullyQualifiedName) *>
write(t.fullyQualifiedName, s"${t.labels.mkString(" - ")} STARTED\n", true, lock)

case t: ExecutionEvent.Test[?] =>
createDebugFile(t.fullyQualifiedName) *>
removeLine(t.fullyQualifiedName, t.labels.mkString(" - ") + " STARTED", lock)

case _ => Kyo.unit

private def write(
fullyQualifiedTaskName: String,
content: String,
append: Boolean,
lock: TestDebugFileLock
): Unit < Any =
lock.updateFile(
Kyo
.acquireReleaseWith(
Kyo.attemptBlockingIO(new java.io.FileWriter(outputFileForTask(fullyQualifiedTaskName), append))
)(f => Kyo.attemptBlocking(f.close()).orPanic) { f =>
Kyo.attemptBlockingIO(f.append(content))
}
.ignore
)

private def removeLine(fullyQualifiedTaskName: String, searchString: String, lock: TestDebugFileLock): Unit < Any =
lock.updateFile {
Kyo.pure {
val file = new File(outputFileForTask(fullyQualifiedTaskName))
if file.exists() then
val source = Source.fromFile(file)

val remainingLines =
source.getLines.filterNot(_.contains(searchString)).toList

val pw = new PrintWriter(outputFileForTask(fullyQualifiedTaskName))
pw.write(remainingLines.mkString("\n") + "\n")
pw.close()
source.close()
end if
}
}
end TestDebug
18 changes: 18 additions & 0 deletions kyo-test/jvm/src/main/scala/kyo/test/TestPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kyo.test

/** `TestPlatform` provides information about the platform tests are being run on to enable platform specific test configuration.
*/
object TestPlatform:

/** Returns whether the current platform is ScalaJS.
*/
final val isJS = false

/** Returns whether the currently platform is the JVM.
*/
final val isJVM = true

/** Returns whether the current platform is Scala Native.
*/
final val isNative = false
end TestPlatform
81 changes: 81 additions & 0 deletions kyo-test/jvm/src/main/scala/kyo/test/results/ResultFileOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package kyo.test.results

import java.io.IOException
import kyo.*
import scala.io.Source

private[test] trait ResultFileOps:
def write(content: => String, append: Boolean): Unit < (IO & Abort[IOException])

private[test] object ResultFileOps:
val live: Layer[ResultFileOps, Any] =
Layer.scoped(
Json.apply
)

private[test] case class Json(resultPath: String, lock: Var[Unit]) extends ResultFileOps:
def write(content: => String, append: Boolean): Unit < (IO & Abort[IOException]) =
lock.updateKyo(_ =>
Kyo
.acquireReleaseWith(Kyo.attemptBlockingIO(new java.io.FileWriter(resultPath, append)))(f =>
Kyo.attemptBlocking(f.close()).orDie
) { f =>
Kyo.attemptBlockingIO(f.append(content))
}
.ignore
)

private val makeOutputDirectory = Kyo.attempt {
import java.nio.file.{Files, Paths}

val fp = Paths.get(resultPath)
Files.createDirectories(fp.getParent)
}.unit

private def closeJson: Unit < (Env[Scope] & Abort[Throwable]) =
removeLastComma *>
write("\n ]\n}", append = true).orDie

private def writeJsonPreamble: Unit < IO =
write(
"""|{
| "results": [""".stripMargin,
append = false
).orDie

private val removeLastComma =
for
source <- Kyo.pure(Source.fromFile(resultPath))
updatedLines =
val lines = source.getLines().toList
if lines.nonEmpty && lines.last.endsWith(",") then
val lastLine = lines.last
val newLastLine = lastLine.dropRight(1)
lines.init :+ newLastLine
else
lines
end if
_ <- Kyo.when(updatedLines.nonEmpty) {
val firstLine :: rest = updatedLines
for
_ <- write(firstLine + "\n", append = false)
_ <- Kyo.foreach(rest)(line => write(line + "\n", append = true))
_ <- Kyo.addFinalizer(Kyo.attempt(source.close()).orDie)
yield ()
end for
}
yield ()
end Json

object Json:
def apply: Json < (Env[Scope] & Abort[Nothing]) =
Kyo.acquireRelease(
for
fileLock <- AtomicRef.init[Unit](())
instance = Json("target/test-reports-zio/output.json", fileLock)
_ <- instance.makeOutputDirectory.orDie
_ <- instance.writeJsonPreamble
yield instance
)(instance => instance.closeJson.orDie)
end Json
end ResultFileOps
Loading
Loading