Developing all standard definitions and initial checks/assertions.
This commit is contained in:
parent
c9b23251d5
commit
789fc594a2
15 changed files with 310 additions and 44 deletions
build.sbt
modules
api-definition/src
main/scala/gs/test/v0/definition
Assertion.scalaCheck.scalaMarker.scalaPermanentId.scalaTag.scalaTestDefinition.scalaTestFailure.scalaTestGroup.scalaTestGroupDefinition.scalaTestIterations.scalaTestSuite.scalasyntax.scala
test/scala/gs/test/v0
core/src/main/scala/gs/test/v0
22
build.sbt
22
build.sbt
|
@ -51,15 +51,29 @@ lazy val testSettings = Seq(
|
|||
|
||||
lazy val `gs-test` = project
|
||||
.in(file("."))
|
||||
.aggregate(core)
|
||||
.aggregate(
|
||||
`api-definition`,
|
||||
`api-execution`
|
||||
)
|
||||
.settings(noPublishSettings)
|
||||
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
|
||||
|
||||
lazy val core = project
|
||||
.in(file("modules/core"))
|
||||
lazy val `api-definition` = project
|
||||
.in(file("modules/api-definition"))
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
|
||||
.settings(name := s"${gsProjectName.value}-api-definition-v${semVerMajor.value}")
|
||||
.settings(libraryDependencies ++= Seq(
|
||||
Deps.Cats.Core,
|
||||
Deps.Cats.Effect,
|
||||
Deps.Fs2.Core
|
||||
))
|
||||
|
||||
lazy val `api-execution` = project
|
||||
.in(file("modules/api-execution"))
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-api-execution-v${semVerMajor.value}")
|
||||
.settings(libraryDependencies ++= Seq(
|
||||
Deps.Cats.Core,
|
||||
Deps.Cats.Effect,
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package gs.test.v0.definition
|
||||
|
||||
import scala.reflect.*
|
||||
|
||||
sealed abstract class Assertion(val name: String)
|
||||
|
||||
object Assertion:
|
||||
|
||||
private def success(): Either[TestFailure, Unit] = Right(())
|
||||
|
||||
def renderInput[A](value: A): String = ""
|
||||
|
||||
// TODO: Code Position
|
||||
|
||||
case object IsEqualTo extends Assertion("isEqualTo"):
|
||||
def evaluate[A: ClassTag](
|
||||
candidate: A,
|
||||
expected: A
|
||||
)(using CanEqual[A, A]): Either[TestFailure, Unit] =
|
||||
if candidate == expected then
|
||||
success()
|
||||
else
|
||||
val runtimeType = classTag[A].runtimeClass.getName()
|
||||
Left(TestFailure.AssertionFailed(
|
||||
assertionName = name,
|
||||
inputs = Map(
|
||||
"candidate" -> runtimeType,
|
||||
"expected" -> runtimeType
|
||||
),
|
||||
message = s"'${renderInput(candidate)}' was not equal to '${renderInput(candidate)}'"
|
||||
))
|
||||
|
||||
case object IsTrue extends Assertion("isTrue"):
|
||||
def evaluate(candidate: Boolean): Either[TestFailure, Unit] =
|
||||
if candidate then
|
||||
success()
|
||||
else
|
||||
Left(TestFailure.AssertionFailed(
|
||||
assertionName = name,
|
||||
inputs = Map("candidate" -> "Boolean"),
|
||||
message = s"Expected '$candidate' to be 'true'."
|
||||
))
|
||||
|
||||
end Assertion
|
|
@ -0,0 +1,35 @@
|
|||
package gs.test.v0.definition
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import cats.effect.Sync
|
||||
|
||||
opaque type Check[A] = A
|
||||
|
||||
object Check:
|
||||
|
||||
type TestResult = Either[TestFailure, Unit]
|
||||
|
||||
def apply[A](candidate: A): Check[A] = candidate
|
||||
|
||||
extension [A: ClassTag](check: Check[A])
|
||||
/**
|
||||
* @return The unwrapped value of this [[Check]].
|
||||
*/
|
||||
def unwrap(): A = check
|
||||
|
||||
def isEqualTo(expected: A)(using CanEqual[A, A]): TestResult =
|
||||
Assertion.IsEqualTo.evaluate(check, expected)
|
||||
|
||||
def isEqualToF[F[_]: Sync](
|
||||
expected: A
|
||||
)(using CanEqual[A, A]): F[TestResult] =
|
||||
Sync[F].delay(isEqualTo(expected))
|
||||
|
||||
extension (check: Check[Boolean])
|
||||
def isTrue(): TestResult =
|
||||
Assertion.IsTrue.evaluate(check)
|
||||
|
||||
def isTrueF[F[_]: Sync](): F[TestResult] =
|
||||
Sync[F].delay(isTrue())
|
||||
|
||||
end Check
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
/**
|
||||
* Enumeration for _Markers_, special tokens which "mark" a test to change
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import cats.Show
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import cats.Show
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.Show
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
/**
|
||||
* Base trait for all failures recognized by gs-test.
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import cats.syntax.all.*
|
||||
import cats.effect.Async
|
||||
|
@ -10,11 +10,41 @@ import scala.jdk.CollectionConverters.*
|
|||
/**
|
||||
* Base class for defining groups of related tests. Users should extend this
|
||||
* class to define their tests.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.definition.*
|
||||
*
|
||||
* final class MyTestGroup extends TestGroup.IO:
|
||||
* override def name: String = "My Test Group"
|
||||
* override def tags: List[Tag] = List(tag"ex1", tag"ex2")
|
||||
* override def documentation: Option[String] = Some("docs")
|
||||
*
|
||||
* test(pid"PermanentId-001", "Example Test Name").pure {
|
||||
* Right(())
|
||||
* }
|
||||
* }}}
|
||||
*/
|
||||
abstract class TestGroup[F[_]: Async]:
|
||||
/**
|
||||
* @return The display name for this group.
|
||||
*/
|
||||
def name: String
|
||||
|
||||
/**
|
||||
* @return List of [[Tag]] that apply to all tests within this group.
|
||||
*/
|
||||
def tags: List[Tag] = List.empty
|
||||
|
||||
/**
|
||||
* @return List of all [[Marker]] that apply to all tests within this group.
|
||||
*/
|
||||
def markers: List[Marker] = List.empty
|
||||
|
||||
/**
|
||||
* @return The documentation for this group.
|
||||
*/
|
||||
def documentation: Option[String] = None
|
||||
|
||||
private var beforeGroupValue: Option[F[Unit]] = None
|
||||
|
@ -24,7 +54,12 @@ abstract class TestGroup[F[_]: Async]:
|
|||
|
||||
private val registry: TestGroup.Registry[F] = new TestGroup.Registry[F]
|
||||
|
||||
def toGroupDefinition(): TestGroupDefinition[F] =
|
||||
/**
|
||||
* Compile the contents of this [[TestGroup]] for delivery to the engine.
|
||||
*
|
||||
* @return The immutable, compiled form of this [[TestGroup]].
|
||||
*/
|
||||
def compile(): TestGroupDefinition[F] =
|
||||
new TestGroupDefinition[F](
|
||||
name = TestGroupDefinition.Name(name),
|
||||
documentation = documentation,
|
||||
|
@ -37,18 +72,40 @@ abstract class TestGroup[F[_]: Async]:
|
|||
tests = registry.toList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Provide an effect that must run before any of the tests within this group
|
||||
* are executed.
|
||||
*
|
||||
* @param f The effect to run.
|
||||
*/
|
||||
protected def beforeGroup(f: => F[Unit]): Unit =
|
||||
beforeGroupValue = Some(f)
|
||||
()
|
||||
|
||||
/**
|
||||
* Provide an effect that must run after all tests within this group have
|
||||
* finished execution.
|
||||
*
|
||||
* @param f The effect to run.
|
||||
*/
|
||||
protected def afterGroup(f: => F[Unit]): Unit =
|
||||
afterGroupValue = Some(f)
|
||||
()
|
||||
|
||||
/**
|
||||
* Provide an effect that must run before each test within this group.
|
||||
*
|
||||
* @param f The effect to run.
|
||||
*/
|
||||
protected def beforeEachTest(f: => F[Unit]): Unit =
|
||||
beforeEachTestValue = Some(f)
|
||||
()
|
||||
|
||||
/**
|
||||
* Provide an effect that must run after each test within this group.
|
||||
*
|
||||
* @param f The effect to run.
|
||||
*/
|
||||
protected def afterEachTest(f: => F[Unit]): Unit =
|
||||
afterEachTestValue = Some(f)
|
||||
()
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import cats.Show
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import cats.Show
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
/**
|
||||
* The Test Suite is the primary unit of organization within `gs-test` -- each
|
|
@ -0,0 +1,141 @@
|
|||
package gs.test.v0.definition
|
||||
|
||||
import cats.Applicative
|
||||
import cats.data.EitherT
|
||||
import cats.effect.Sync
|
||||
import cats.syntax.all.*
|
||||
|
||||
/**
|
||||
* String interpolator for [[Tag]]. Shorthand for producing new [[Tag]]
|
||||
* instances.
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.definition.*
|
||||
* val tag1: TestDefinition.Tag = tag"example"
|
||||
* }}}
|
||||
*/
|
||||
extension (sc: StringContext)
|
||||
def tag(args: Any*): Tag = Tag(sc.s(args*))
|
||||
|
||||
/**
|
||||
* String interpolator for [[PermanentId]]. Shorthand for producing new
|
||||
* [[PermanentId]] instances.
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.definition.*
|
||||
* val permanentId: PermanentId = pid"example"
|
||||
* }}}
|
||||
*/
|
||||
extension (sc: StringContext)
|
||||
def pid(args: Any*): PermanentId = PermanentId(sc.s(args*))
|
||||
|
||||
/**
|
||||
* Request this test to fail (pure form).
|
||||
*
|
||||
* @param message The message to report - why did this test fail?
|
||||
* @return The failing test result.
|
||||
*/
|
||||
def fail(message: String): Either[TestFailure, Unit] =
|
||||
Left(TestFailure.TestRequestedFailure(message))
|
||||
|
||||
/**
|
||||
* Request this test to fail (lifted into F).
|
||||
*
|
||||
* @param message The message to report - why did this test fail?
|
||||
* @return The failing test result.
|
||||
*/
|
||||
def failF[F[_]: Applicative](message: String): F[Either[TestFailure, Unit]] =
|
||||
Applicative[F].pure(fail(message))
|
||||
|
||||
/**
|
||||
* Request this test to fail (lifted into EitherT).
|
||||
*
|
||||
* @param message The message to report - why did this test fail?
|
||||
* @return The failing test result.
|
||||
*/
|
||||
def failT[F[_]: Applicative](message: String): EitherT[F, TestFailure, Unit] =
|
||||
EitherT(failF(message))
|
||||
|
||||
/**
|
||||
* Shorthand for indicating a passing test (pure form).
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.definition.*
|
||||
*
|
||||
* final class Example extends TestGroup.IO:
|
||||
* override def name: String = "example"
|
||||
*
|
||||
* test(pid"ex", "Example Test").pure { pass() }
|
||||
* }}}
|
||||
*
|
||||
* @return The passing test result.
|
||||
*/
|
||||
def pass(): Either[TestFailure, Unit] = Right(())
|
||||
|
||||
/**
|
||||
* Shorthand for indicating a passing test (lifted into F).
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.definition.*
|
||||
*
|
||||
* final class Example extends TestGroup.IO:
|
||||
* override def name: String = "example"
|
||||
*
|
||||
* test(pid"ex", "Example Test").effectful { passF() }
|
||||
* }}}
|
||||
*
|
||||
* @return The passing test result.
|
||||
*/
|
||||
def passF[F[_]: Applicative](): F[Either[TestFailure, Unit]] =
|
||||
Applicative[F].pure(Right(()))
|
||||
|
||||
/**
|
||||
* Shorthand for indicating a passing test (lifted into EitherT).
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.definition.*
|
||||
*
|
||||
* final class Example extends TestGroup.IO:
|
||||
* override def name: String = "example"
|
||||
*
|
||||
* test(pid"ex", "Example Test") { passT() }
|
||||
* }}}
|
||||
*
|
||||
* @return The passing test result.
|
||||
*/
|
||||
def passT[F[_]: Applicative](): EitherT[F, TestFailure, Unit] =
|
||||
EitherT(passF())
|
||||
|
||||
/**
|
||||
* Check all of the given results, returning the first failure, or a successful
|
||||
* result if no result failed.
|
||||
*
|
||||
* @param results The list of results to check.
|
||||
* @return Successful result or the first failure.
|
||||
*/
|
||||
def checkAll(
|
||||
results: Either[TestFailure, Unit]*
|
||||
): Either[TestFailure, Unit] =
|
||||
val initial: Either[TestFailure, Unit] = Right(())
|
||||
results.foldLeft(initial) { (acc, result) =>
|
||||
acc match
|
||||
case Left(_) => acc
|
||||
case Right(_) => result
|
||||
}
|
||||
|
||||
def checkAllF[F[_]: Sync](
|
||||
checks: F[Either[TestFailure, Unit]]*
|
||||
): F[Either[TestFailure, Unit]] =
|
||||
val initial: F[Either[TestFailure, Unit]] = Sync[F].delay(Right(()))
|
||||
checks.foldLeft(initial) { (acc, result) =>
|
||||
acc.flatMap {
|
||||
case Right(_) => result
|
||||
case err => Sync[F].pure(err)
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
package gs.test.v0
|
||||
package gs.test.v0.definition
|
||||
|
||||
import munit.*
|
||||
import cats.effect.IO
|
||||
|
||||
import gs.test.v0.{Tag => GsTag}
|
||||
import gs.test.v0.definition.{Tag => GsTag}
|
||||
|
||||
class GroupImplementationTests extends FunSuite:
|
||||
import GroupImplementationTests.*
|
||||
|
||||
test("should support a group with a simple, pure, test") {
|
||||
val g1 = new G1
|
||||
val group = g1.toGroupDefinition()
|
||||
val group = g1.compile()
|
||||
assertEquals(group.name, TestGroupDefinition.Name("G1"))
|
||||
assertEquals(group.documentation, None)
|
||||
assertEquals(group.testTags, List.empty)
|
||||
|
@ -34,7 +34,7 @@ class GroupImplementationTests extends FunSuite:
|
|||
|
||||
test("should support a group with all values set") {
|
||||
val g2 = new G2
|
||||
val group = g2.toGroupDefinition()
|
||||
val group = g2.compile()
|
||||
assertEquals(group.name, TestGroupDefinition.Name("G2"))
|
||||
assertEquals(group.documentation, Some("docs"))
|
||||
assertEquals(group.testTags, List(GsTag("tag")))
|
||||
|
@ -58,7 +58,7 @@ class GroupImplementationTests extends FunSuite:
|
|||
|
||||
test("should support a simple group with a configured test") {
|
||||
val g3 = new G3
|
||||
val group = g3.toGroupDefinition()
|
||||
val group = g3.compile()
|
||||
assertEquals(group.name, TestGroupDefinition.Name("G3"))
|
||||
assertEquals(group.documentation, None)
|
||||
assertEquals(group.testTags, List.empty)
|
|
@ -1,25 +0,0 @@
|
|||
package gs.test.v0
|
||||
|
||||
/**
|
||||
* String interpolator for [[Tag]]. Shorthand for producing new [[Tag]]
|
||||
* instances.
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.*
|
||||
* val tag1: TestDefinition.Tag = tag"example"
|
||||
* }}}
|
||||
*/
|
||||
extension (sc: StringContext)
|
||||
def tag(args: Any*): Tag = Tag(sc.s(args*))
|
||||
|
||||
/**
|
||||
* String interpolator for [[PermanentId]]. Shorthand for producing new
|
||||
* [[PermanentId]] instances.
|
||||
*
|
||||
* {{{
|
||||
* import gs.test.v0.*
|
||||
* val permanentId: PermanentId = pid"example"
|
||||
* }}}
|
||||
*/
|
||||
extension (sc: StringContext)
|
||||
def pid(args: Any*): PermanentId = PermanentId(sc.s(args*))
|
Loading…
Add table
Reference in a new issue