
doobie.util.testing.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of doobie-core_2.13 Show documentation
Show all versions of doobie-core_2.13 Show documentation
Pure functional JDBC layer for Scala.
// Copyright (c) 2013-2020 Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT
package doobie.util
import cats.data.NonEmptyList
import cats.effect.kernel.Async
import cats.instances.int.*
import cats.instances.list.*
import cats.instances.string.*
import cats.syntax.list.*
import cats.syntax.applicativeError.*
import cats.syntax.foldable.*
import cats.syntax.show.*
import doobie.*
import doobie.implicits.*
import doobie.util.analysis.*
import doobie.util.pretty.*
import doobie.util.pos.Pos
import scala.Predef.augmentString
import org.tpolecat.typename.*
package testing {
trait UnsafeRun[F[_]] {
def unsafeRunSync[A](fa: F[A]): A
}
/** Common base trait for various checkers and matchers.
*/
trait CheckerBase[M[_]] {
// Effect type, required instances
implicit def M: Async[M]
implicit def U: UnsafeRun[M]
def transactor: Transactor[M]
def colors: Colors = Colors.Ansi
}
/** Common data for all query-like types. */
final case class AnalysisArgs(
typeName: String,
pos: Option[Pos],
sql: String,
analysis: ConnectionIO[Analysis]
) {
val cleanedSql = Block(
sql.linesIterator
.map(_.trim)
.filterNot(_.isEmpty)
.toList
)
private val location =
pos
.map(f => show"${f.file}:${f.line}")
.getOrElse("(source location unknown)")
val header: String = show"$typeName defined at $location"
}
/** Information from [[Analysis]], prepared for output. */
final case class AnalysisReport(
header: String,
sql: Block,
items: List[AnalysisReport.Item]
) {
val succeeded: Boolean = items.forall(_.error.isEmpty)
}
object AnalysisReport {
final case class Item(description: String, error: Option[Block])
}
/** Typeclass for query-like objects. */
trait Analyzable[T] {
def unpack(t: T): AnalysisArgs
}
object Analyzable {
def apply[T](implicit ev: Analyzable[T]): Analyzable[T] = ev
def unpack[T](t: T)(implicit T: Analyzable[T]): AnalysisArgs =
T.unpack(t)
def instance[T](
impl: T => AnalysisArgs
): Analyzable[T] =
new Analyzable[T] {
def unpack(t: T) = impl(t)
}
implicit def analyzableQuery[A: TypeName, B: TypeName]: Analyzable[Query[A, B]] =
instance { q =>
AnalysisArgs(
s"Query[${typeName[A]}, ${typeName[B]}]",
q.pos,
q.sql,
q.analysis
)
}
implicit def analyzableQuery0[A: TypeName]: Analyzable[Query0[A]] =
instance { q =>
AnalysisArgs(
s"Query0[${typeName[A]}]",
q.pos,
q.sql,
q.analysis
)
}
implicit def analyzableUpdate[A: TypeName]: Analyzable[Update[A]] =
instance { q =>
AnalysisArgs(
s"Update[${typeName[A]}]",
q.pos,
q.sql,
q.analysis
)
}
implicit val analyzableUpdate0: Analyzable[Update0] =
instance { q =>
AnalysisArgs(
s"Update0",
q.pos,
q.sql,
q.analysis
)
}
}
}
/** Common utilities for query testing
*/
package object testing {
def analyze(args: AnalysisArgs): ConnectionIO[AnalysisReport] =
args.analysis.attempt
.map(buildItems)
.map { items =>
AnalysisReport(
args.header,
args.cleanedSql,
items
)
}
private def alignmentErrorsToBlock(
es: NonEmptyList[AlignmentError]
): Block =
Block(es.toList.flatMap(_.msg.linesIterator))
private def buildItems(
input: Either[Throwable, Analysis]
): List[AnalysisReport.Item] = input match {
case Left(e) =>
List(AnalysisReport.Item(
"SQL Compiles and TypeChecks",
Some(Block.fromErrorMsgLines(e))
))
case Right(a) =>
AnalysisReport.Item("SQL Compiles and TypeChecks", None) ::
(a.paramDescriptions ++ a.columnDescriptions)
.map { case (s, es) =>
AnalysisReport.Item(s, es.toNel.map(alignmentErrorsToBlock))
}
}
/** Simple formatting for analysis results.
*/
def formatReport(
args: AnalysisArgs,
report: AnalysisReport,
colors: Colors
): Block = {
val sql = args.cleanedSql
.wrap(68)
// SQL should use the default color
.padLeft(colors.RESET.toString)
val items = report.items.foldMap(formatItem(colors))
Block.fromString(args.header)
.above(sql)
.above(items)
}
private def formatItem(colors: Colors): AnalysisReport.Item => Block = {
case AnalysisReport.Item(desc, None) =>
Block.fromString(s"${colors.GREEN}✓${colors.RESET} $desc")
case AnalysisReport.Item(desc, Some(err)) =>
Block.fromString(s"${colors.RED}✕${colors.RESET} $desc")
// No color for error details - ScalaTest paints each line of failure
// red by default.
.above(err.wrap(66).padLeft(" "))
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy