e.probably-core.0.3.0.source-code.probably.scala Maven / Gradle / Ivy
The newest version!
/*
Probably, version 0.18.0. Copyright 2017-22 Jon Pretty, Propensive OÜ.
The primary distribution site is: https://propensive.com/
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the License for the specific language governing permissions
and limitations under the License.
*/
package probably
import wisteria.*
import escritoire.*
import rudiments.*
import gossamer.*
import eucalyptus.*
import iridescence.*
import escapade.*
import scala.collection.mutable.HashMap
import scala.util.*
import scala.quoted.*
import language.dynamics
given realm: Realm = Realm(t"probably")
import Runner.*
object Runner:
case class TestId private[probably](value: Text)
enum Outcome:
case FailsAt(datapoint: Datapoint, count: Int)
case Passed
case Mixed
def failed: Boolean = !passed
def passed: Boolean = this == Passed
def filename: Text = this match
case FailsAt(datapoint, count) => datapoint.debugValue.filename.otherwise(t"(no source)")
case _ => t""
def line: Text = this match
case FailsAt(datapoint, count) => datapoint.debugValue.line.otherwise(0).show
case _ => t""
def debug: Text = this match
case FailsAt(datapoint, count) =>
val padWidth = datapoint.debugValue.allInfo.map(_(0).length).max
datapoint.debugValue.allInfo.map { case (k, v) =>
val value: Text = (v.cut(t"\n"): List[Text]).join(t"\n${t" "*(padWidth + 3)}")
t"${k.pad(padWidth, Ltr)} = $value"
}.join(t"\n")
case _ =>
t""
enum Datapoint(val debugValue: Debug):
case Pass extends Datapoint(Debug())
case Fail(debug: Debug, index: Int) extends Datapoint(debug)
case Throws(exception: Throwable, debug: Debug) extends Datapoint(debug)
case PredicateThrows(exception: Exception, debug: Debug, index: Int) extends Datapoint(debug)
def shortDigest[T: Show](text: T): Text =
val md = java.security.MessageDigest.getInstance("SHA-256").nn
md.update(text.show.s.getBytes)
md.digest.nn.take(3).unsafeImmutable.map { b => Text(f"$b%02x") }.join
class Runner(subset: Set[TestId] = Set()) extends Dynamic:
val runner: Runner = this
final def skip(testId: TestId): Boolean = !(subset.isEmpty || subset(testId))
def apply[T](name: Text)(fn: Test ?=> T): Test { type Type = T } =
new Test(name):
type Type = T
def action(): T = fn(using this)
def time[T](name: Text)(fn: => T)(using Log): T = apply[T](name)(fn).check { _ => true }
def suite(testSuite: TestSuite)(using Log): Unit = suite(testSuite.name)(testSuite.run)
def suite(name: Text)(fn: Runner ?=> Unit)(using Log): Unit =
Log.info(ansi"Starting test suite ${colors.Gold}($name)")
val test = new Test(name):
type Type = Report
def action(): Report =
val runner = Runner()
fn(using runner)
runner.report()
val t0 = System.currentTimeMillis
val report = test.check(_.results.forall(_.outcome.passed))
val t1 = System.currentTimeMillis - t0
if report.results.exists(_.outcome.passed) && report.results.exists(_.outcome.failed)
then synchronized {
results = results.updated(name, results(name).copy(outcome = Outcome.Mixed))
}
report.results.foreach { result => record(result.copy(indent = result.indent + 1)) }
Log.fine(ansi"Completed test suite ${colors.Gold}($name) in ${t1.show}ms")
def assert(name: Text)(fn: => Boolean)(using Log): Unit = apply(name)(fn).oldAssert(identity)
object Test:
given AnsiShow[Test] = test => ansi"${colors.Khaki}(${test.id.value})"
abstract class Test(val name: Text):
type Type
def id: TestId = TestId(Runner.shortDigest(name))
def action(): Type
def oldAssert(pred: Type => Boolean)(using Log): Unit =
try if !skip(id) then check(pred)
catch case NonFatal(e) =>
Log.warn(ansi"A $e exception was thrown whilst checking the test ${this.ansi}")
private val map: HashMap[Text, Text] = HashMap()
def debug[T](name: Text, expr: T): T =
map(name) = Showable(expr).show
expr
inline def assert(inline pred: Type => Boolean)(using log: Log): Unit =
${Macros.assert[Type]('runner, 'this, 'pred, 'log)}
def check(pred: Type => Boolean, debug: Option[Type] => Debug = v => Debug(v.map(Showable(_).show)))
(using Log): Type =
def handler(index: Int): PartialFunction[Throwable, Datapoint] =
case e: Exception =>
Log.warn(ansi"A $e exception was thrown in the test ${this.ansi}")
Datapoint.PredicateThrows(e, Debug(), index)
def makeDatapoint(pred: Type => Boolean, count: Int, datapoint: Datapoint, value: Type)
: Datapoint =
try if pred(value) then Datapoint.Pass else Datapoint.Fail(debug(Some(value)), count)
catch handler(count)
val t0 = System.currentTimeMillis()
val value = Try(action())
val time = System.currentTimeMillis() - t0
value match
case Success(value) =>
record(this, time, makeDatapoint(pred, 0, Datapoint.Pass, value))
value
case Failure(e) =>
val trace = Option(e.getStackTrace).fold(Nil)(_.nn.unsafeImmutable.to(List).map(_.nn))
val info = trace.takeWhile(_.getClassName != "probably.Suite")
.map { frame => Showable(frame.nn).show }
.join(Option(e.getMessage).fold(t"null") { x => t"${x.nn}\n at " }, t"\n at ", t"")
record(this, time, Datapoint.Throws(e, debug(None).add(t"exception", info)))
throw e
def report(): Report = Report(results.values.to(List))
def clear(): Unit = results = emptyResults()
protected def record(test: Test, duration: Long, datapoint: Datapoint): Unit = synchronized:
results = results.updated(test.name, results(test.name).append(test.name, duration, datapoint))
protected def record(summary: Summary) = synchronized:
results = results.updated(summary.name, summary)
private def emptyResults(): Map[Text, Summary] = ListMap().withDefault:
name =>
Summary(TestId(shortDigest(name)), name, 0, Int.MaxValue, 0L, Int.MinValue, Outcome.Passed, 0)
@volatile
protected var results: Map[Text, Summary] = emptyResults()
end Runner
case class Debug(found: Option[Text] = None, filename: Maybe[Text] = Unset, line: Maybe[Int] = Unset,
expected: Maybe[Text] = Unset, info: Map[Text, Text] = Map()):
def add(key: Text, value: Text): Debug = copy(info = info.updated(key, value))
def allInfo: ListMap[Text, Text] =
val basicInfo =
List(t"found" -> found.getOrElse(Unset), t"expected" -> expected)
.filter(_._2 != Unset)
.to(ListMap)
.view
.mapValues(Showable(_).show)
.to(ListMap)
basicInfo ++ info
object Macros:
import scala.reflect.*
def assert[T: Type](runner: Expr[Runner], test: Expr[Runner#Test { type Type = T }], pred: Expr[T => Boolean], log: Expr[Log])(using Quotes): Expr[Unit] =
import quotes.reflect.*
val filename: Expr[String] = Expr:
val absolute = Showable(Position.ofMacroExpansion).show.cut(t":").head
val pwd = try Sys.user.dir().show catch case e: KeyNotFoundError => throw Impossible("should not happen")
if absolute.startsWith(pwd) then absolute.drop(pwd.length + 1).s else absolute.s
val line = Expr(Position.ofMacroExpansion.startLine + 1)
def debugExpr[S: Type](expr: Expr[S]): Expr[Option[S] => Debug] =
val debugString = '{DebugString.showAny}
Expr.summon[Comparison[S]].fold {
'{ (result: Option[S]) =>
Debug(
found = result.map($debugString.show(_)),
filename = Text($filename),
line = $line,
expected = $debugString.show($expr)
)
}
} { comparison =>
'{ (result: Option[S]) =>
Debug(
found = result.map($debugString.show(_)),
filename = Text($filename),
line = $line,
expected = $debugString.show($expr),
info = if result.isEmpty then Map()
else Map(t"structure" -> Showable($comparison.compare(result.get, $expr)).show)
)
}
}
def interpret(pred: Expr[T => Boolean]): Expr[Option[T] => Debug] = pred match
case '{ (x: T) => x == ($expr: T) } => debugExpr(expr)
case '{ (x: T) => ($expr: T) == x } => debugExpr(expr)
case '{ (x: Double) => x == ($expr: Double) } => debugExpr(expr)
case '{ (x: Double) => x == ($expr: Float) } => debugExpr(expr)
case '{ (x: Double) => x == ($expr: Long) } => debugExpr(expr)
case '{ (x: Double) => x == ($expr: Int) } => debugExpr(expr)
case '{ (x: Double) => x == ($expr: Short) } => debugExpr(expr)
case '{ (x: Double) => x == ($expr: Byte) } => debugExpr(expr)
case '{ (x: Float) => x == ($expr: Double) } => debugExpr(expr)
case '{ (x: Float) => x == ($expr: Float) } => debugExpr(expr)
case '{ (x: Float) => x == ($expr: Long) } => debugExpr(expr)
case '{ (x: Float) => x == ($expr: Int) } => debugExpr(expr)
case '{ (x: Float) => x == ($expr: Short) } => debugExpr(expr)
case '{ (x: Float) => x == ($expr: Byte) } => debugExpr(expr)
case '{ (x: Long) => x == ($expr: Double) } => debugExpr(expr)
case '{ (x: Long) => x == ($expr: Float) } => debugExpr(expr)
case '{ (x: Long) => x == ($expr: Long) } => debugExpr(expr)
case '{ (x: Long) => x == ($expr: Int) } => debugExpr(expr)
case '{ (x: Long) => x == ($expr: Short) } => debugExpr(expr)
case '{ (x: Long) => x == ($expr: Byte) } => debugExpr(expr)
case '{ (x: Int) => x == ($expr: Double) } => debugExpr(expr)
case '{ (x: Int) => x == ($expr: Float) } => debugExpr(expr)
case '{ (x: Int) => x == ($expr: Long) } => debugExpr(expr)
case '{ (x: Int) => x == ($expr: Int) } => debugExpr(expr)
case '{ (x: Int) => x == ($expr: Short) } => debugExpr(expr)
case '{ (x: Int) => x == ($expr: Byte) } => debugExpr(expr)
case '{ (x: Short) => x == ($expr: Double) } => debugExpr(expr)
case '{ (x: Short) => x == ($expr: Float) } => debugExpr(expr)
case '{ (x: Short) => x == ($expr: Long) } => debugExpr(expr)
case '{ (x: Short) => x == ($expr: Int) } => debugExpr(expr)
case '{ (x: Short) => x == ($expr: Short) } => debugExpr(expr)
case '{ (x: Short) => x == ($expr: Byte) } => debugExpr(expr)
case '{ (x: Byte) => x == ($expr: Double) } => debugExpr(expr)
case '{ (x: Byte) => x == ($expr: Float) } => debugExpr(expr)
case '{ (x: Byte) => x == ($expr: Long) } => debugExpr(expr)
case '{ (x: Byte) => x == ($expr: Int) } => debugExpr(expr)
case '{ (x: Byte) => x == ($expr: Short) } => debugExpr(expr)
case '{ (x: Byte) => x == ($expr: Byte) } => debugExpr(expr)
case '{ (x: Boolean) => x == ($expr: Boolean) } => debugExpr(expr)
case '{ (x: Double) => ($expr: Double) == x } => debugExpr(expr)
case '{ (x: Double) => ($expr: Float) == x } => debugExpr(expr)
case '{ (x: Double) => ($expr: Long) == x } => debugExpr(expr)
case '{ (x: Double) => ($expr: Int) == x } => debugExpr(expr)
case '{ (x: Double) => ($expr: Short) == x } => debugExpr(expr)
case '{ (x: Double) => ($expr: Byte) == x } => debugExpr(expr)
case '{ (x: Float) => ($expr: Double) == x } => debugExpr(expr)
case '{ (x: Float) => ($expr: Float) == x } => debugExpr(expr)
case '{ (x: Float) => ($expr: Long) == x } => debugExpr(expr)
case '{ (x: Float) => ($expr: Int) == x } => debugExpr(expr)
case '{ (x: Float) => ($expr: Short) == x } => debugExpr(expr)
case '{ (x: Float) => ($expr: Byte) == x } => debugExpr(expr)
case '{ (x: Long) => ($expr: Double) == x } => debugExpr(expr)
case '{ (x: Long) => ($expr: Float) == x } => debugExpr(expr)
case '{ (x: Long) => ($expr: Long) == x } => debugExpr(expr)
case '{ (x: Long) => ($expr: Int) == x } => debugExpr(expr)
case '{ (x: Long) => ($expr: Short) == x } => debugExpr(expr)
case '{ (x: Long) => ($expr: Byte) == x } => debugExpr(expr)
case '{ (x: Int) => ($expr: Double) == x } => debugExpr(expr)
case '{ (x: Int) => ($expr: Float) == x } => debugExpr(expr)
case '{ (x: Int) => ($expr: Long) == x } => debugExpr(expr)
case '{ (x: Int) => ($expr: Int) == x } => debugExpr(expr)
case '{ (x: Int) => ($expr: Short) == x } => debugExpr(expr)
case '{ (x: Int) => ($expr: Byte) == x } => debugExpr(expr)
case '{ (x: Short) => ($expr: Double) == x } => debugExpr(expr)
case '{ (x: Short) => ($expr: Float) == x } => debugExpr(expr)
case '{ (x: Short) => ($expr: Long) == x } => debugExpr(expr)
case '{ (x: Short) => ($expr: Int) == x } => debugExpr(expr)
case '{ (x: Short) => ($expr: Short) == x } => debugExpr(expr)
case '{ (x: Short) => ($expr: Byte) == x } => debugExpr(expr)
case '{ (x: Byte) => ($expr: Double) == x } => debugExpr(expr)
case '{ (x: Byte) => ($expr: Float) == x } => debugExpr(expr)
case '{ (x: Byte) => ($expr: Long) == x } => debugExpr(expr)
case '{ (x: Byte) => ($expr: Int) == x } => debugExpr(expr)
case '{ (x: Byte) => ($expr: Short) == x } => debugExpr(expr)
case '{ (x: Byte) => ($expr: Byte) == x } => debugExpr(expr)
case '{ (x: Boolean) => ($expr: Boolean) == x } => debugExpr(expr)
case expr =>
'{ (opt: Option[?]) => Debug(found = opt.map(_.debug), filename = Text($filename),
line = $line) }
'{
try if !$runner.skip($test.id) then $test.check($pred, ${interpret(pred)})(using $log)
catch case NonFatal(_) => ()
}
object Differences:
given Show[Differences] = diff =>
val table = Tabulation[(List[Text], Text, Text)](
Column(t"Key", _(0).join(t".")),
Column(t"Found", _(1)),
Column(t"Expected", _(2)),
)
table.tabulate(100, diff.flatten).join(t"\n")
enum Differences:
case Same
case Structural(differences: Map[Text, Differences])
case Diff(left: Text, right: Text)
def flatten: Seq[(List[Text], Text, Text)] = this match
case Same => Nil
case Structural(diffs) => diffs.to(List).flatMap { case (label, nested) =>
nested.flatten.map { case (path, left, right) =>
(label :: path, left, right)
}
}
case Diff(left, right) => List((Nil, left, right))
trait Comparison[-T]:
def compare(left: T, right: T): Differences
object Comparison extends Derivation[Comparison]:
given Comparison[String] =
(a, b) => if a == b then Differences.Same else Differences.Diff(Text(a), Text(b))
given [T: Show]: Comparison[T] =
(a, b) => if a == b then Differences.Same else Differences.Diff(a.show, b.show)
def join[T](caseClass: CaseClass[Comparison, T]): Comparison[T] = (left, right) =>
if left == right then Differences.Same
else Differences.Structural {
caseClass.params
.filter { p => p.deref(left) != p.deref(right) }
.map { p => Text(p.label) -> p.typeclass.compare(p.deref(left), p.deref(right)) }
.to(Map)
}
def split[T](sealedTrait: SealedTrait[Comparison, T]): Comparison[T] = (left, right) =>
val leftType = sealedTrait.choose(left)(identity(_))
sealedTrait.choose(right) { subtype =>
if leftType == subtype
then subtype.typeclass.compare(subtype.cast(left), subtype.cast(right))
else Differences.Diff(t"type: ${leftType.typeInfo.short}", t"type: ${subtype.typeInfo.short}")
}
case class Summary(id: TestId, name: Text, count: Int, tmin: Long, ttot: Long, tmax: Long,
outcome: Outcome, indent: Int):
def avg: Double = ttot.toDouble/count/1000.0
def min: Double = tmin.toDouble/1000.0
def max: Double = tmax.toDouble/1000.0
def aggregate(datapoint: Datapoint) = outcome match
case Outcome.FailsAt(dp, c) => Outcome.FailsAt(dp, c)
case Outcome.Passed => datapoint match
case Datapoint.Pass => Outcome.Passed
case other => Outcome.FailsAt(other, count + 1)
case Outcome.Mixed => Outcome.FailsAt(datapoint, 0)
def append(test: Text, duration: Long, datapoint: Datapoint): Summary =
Summary(id, name, count + 1, tmin min duration, ttot + duration, tmax max duration,
aggregate(datapoint), 0)
case class Report(results: List[Summary]):
val passed: Int = results.count(_.outcome == Outcome.Passed)
val failed: Int = results.count(_.outcome != Outcome.Passed)
val total: Int = failed + passed
object TestSuite:
given AnsiShow[TestSuite] = ts => ansi"${colors.Gold}(${ts.name})"
trait TestSuite:
def run(using Runner): Unit
def name: Text
object global:
object test extends Runner()
def test[T](name: Text)(fn: Runner#Test ?=> T)(using runner: Runner, log: Log)
: runner.Test { type Type = T } =
runner(name)(fn)
def suite(name: Text)(fn: Runner ?=> Unit)(using runner: Runner, log: Log): Unit =
runner.suite(name)(fn)
def suite(suite: TestSuite)(using runner: Runner, log: Log): Unit = runner.suite(suite)
def time[T](name: Text)(fn: => T)(using Runner, Log): T = test(name)(fn).check { _ => true }
case class UnexpectedSuccessError(value: Any) extends Error:
def message: Text = t"the expression was expected to throw an exception, but did not"
def capture(fn: => Any): Exception =
try
val result = fn
throw UnexpectedSuccessError(result)
catch
case error: UnexpectedSuccessError => throw error
case error: Exception => error
© 2015 - 2025 Weber Informatics LLC | Privacy Policy