Pre-commit.

This commit is contained in:
Pat Garrity 2024-09-03 22:05:51 -05:00
parent 789fc594a2
commit 79e6672bdf
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
15 changed files with 519 additions and 440 deletions

View file

@ -35,7 +35,7 @@ val Deps = new {
} }
val Gs = new { val Gs = new {
val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.3.0" val Uuid: ModuleID = "gs" %% "gs-uuid-v0" % "0.3.0"
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.2.0" val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.2.0"
} }
@ -44,7 +44,7 @@ val Deps = new {
lazy val testSettings = Seq( lazy val testSettings = Seq(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
Deps.MUnit % Test, Deps.MUnit % Test,
Deps.Gs.Datagen % Test Deps.Gs.Datagen % Test
) )
) )
@ -62,20 +62,28 @@ lazy val `api-definition` = project
.in(file("modules/api-definition")) .in(file("modules/api-definition"))
.settings(sharedSettings) .settings(sharedSettings)
.settings(testSettings) .settings(testSettings)
.settings(name := s"${gsProjectName.value}-api-definition-v${semVerMajor.value}") .settings(
.settings(libraryDependencies ++= Seq( name := s"${gsProjectName.value}-api-definition-v${semVerMajor.value}"
Deps.Cats.Core, )
Deps.Cats.Effect, .settings(
Deps.Fs2.Core libraryDependencies ++= Seq(
)) Deps.Cats.Core,
Deps.Cats.Effect,
Deps.Fs2.Core
)
)
lazy val `api-execution` = project lazy val `api-execution` = project
.in(file("modules/api-execution")) .in(file("modules/api-execution"))
.settings(sharedSettings) .settings(sharedSettings)
.settings(testSettings) .settings(testSettings)
.settings(name := s"${gsProjectName.value}-api-execution-v${semVerMajor.value}") .settings(
.settings(libraryDependencies ++= Seq( name := s"${gsProjectName.value}-api-execution-v${semVerMajor.value}"
Deps.Cats.Core, )
Deps.Cats.Effect, .settings(
Deps.Fs2.Core libraryDependencies ++= Seq(
)) Deps.Cats.Core,
Deps.Cats.Effect,
Deps.Fs2.Core
)
)

View file

@ -13,32 +13,40 @@ object Assertion:
// TODO: Code Position // TODO: Code Position
case object IsEqualTo extends Assertion("isEqualTo"): case object IsEqualTo extends Assertion("isEqualTo"):
def evaluate[A: ClassTag]( def evaluate[A: ClassTag](
candidate: A, candidate: A,
expected: A expected: A
)(using CanEqual[A, A]): Either[TestFailure, Unit] = )(
if candidate == expected then using
success() CanEqual[A, A]
): Either[TestFailure, Unit] =
if candidate == expected then success()
else else
val runtimeType = classTag[A].runtimeClass.getName() val runtimeType = classTag[A].runtimeClass.getName()
Left(TestFailure.AssertionFailed( Left(
assertionName = name, TestFailure.AssertionFailed(
inputs = Map( assertionName = name,
"candidate" -> runtimeType, inputs = Map(
"expected" -> runtimeType "candidate" -> runtimeType,
), "expected" -> runtimeType
message = s"'${renderInput(candidate)}' was not equal to '${renderInput(candidate)}'" ),
)) message =
s"'${renderInput(candidate)}' was not equal to '${renderInput(candidate)}'"
)
)
case object IsTrue extends Assertion("isTrue"): case object IsTrue extends Assertion("isTrue"):
def evaluate(candidate: Boolean): Either[TestFailure, Unit] = def evaluate(candidate: Boolean): Either[TestFailure, Unit] =
if candidate then if candidate then success()
success()
else else
Left(TestFailure.AssertionFailed( Left(
assertionName = name, TestFailure.AssertionFailed(
inputs = Map("candidate" -> "Boolean"), assertionName = name,
message = s"Expected '$candidate' to be 'true'." inputs = Map("candidate" -> "Boolean"),
)) message = s"Expected '$candidate' to be 'true'."
)
)
end Assertion end Assertion

View file

@ -1,7 +1,7 @@
package gs.test.v0.definition package gs.test.v0.definition
import scala.reflect.ClassTag
import cats.effect.Sync import cats.effect.Sync
import scala.reflect.ClassTag
opaque type Check[A] = A opaque type Check[A] = A
@ -12,20 +12,29 @@ object Check:
def apply[A](candidate: A): Check[A] = candidate def apply[A](candidate: A): Check[A] = candidate
extension [A: ClassTag](check: Check[A]) extension [A: ClassTag](check: Check[A])
/** /** @return
* @return The unwrapped value of this [[Check]]. * The unwrapped value of this [[Check]].
*/ */
def unwrap(): A = check def unwrap(): A = check
def isEqualTo(expected: A)(using CanEqual[A, A]): TestResult = def isEqualTo(
expected: A
)(
using
CanEqual[A, A]
): TestResult =
Assertion.IsEqualTo.evaluate(check, expected) Assertion.IsEqualTo.evaluate(check, expected)
def isEqualToF[F[_]: Sync]( def isEqualToF[F[_]: Sync](
expected: A expected: A
)(using CanEqual[A, A]): F[TestResult] = )(
using
CanEqual[A, A]
): F[TestResult] =
Sync[F].delay(isEqualTo(expected)) Sync[F].delay(isEqualTo(expected))
extension (check: Check[Boolean]) extension (check: Check[Boolean])
def isTrue(): TestResult = def isTrue(): TestResult =
Assertion.IsTrue.evaluate(check) Assertion.IsTrue.evaluate(check)

View file

@ -1,22 +1,21 @@
package gs.test.v0.definition package gs.test.v0.definition
/** /** Enumeration for _Markers_, special tokens which "mark" a test to change
* Enumeration for _Markers_, special tokens which "mark" a test to change * execution functionality.
* execution functionality. *
* * The basic case for this enumeration is allowing tests to be ignored.
* The basic case for this enumeration is allowing tests to be ignored. *
* * @param name
* @param name The formal serialized name of the marker. * The formal serialized name of the marker.
*/ */
sealed abstract class Marker(val name: String) sealed abstract class Marker(val name: String)
object Marker: object Marker:
given CanEqual[Marker, Marker] = CanEqual.derived given CanEqual[Marker, Marker] = CanEqual.derived
/** /** If this [[Marker]] is present on a test, the test will be ignored.
* If this [[Marker]] is present on a test, the test will be ignored. */
*/
case object Ignored extends Marker("ignored") case object Ignored extends Marker("ignored")
end Marker end Marker

View file

@ -2,8 +2,7 @@ package gs.test.v0.definition
import cats.Show import cats.Show
/** /** Opaque type representing some _permanent identifier_. These are
* Opaque type representing some _permanent identifier_. These are
* user-assigned strings that are expected to _not change over time_ for some * user-assigned strings that are expected to _not change over time_ for some
* test. This allows tests to be deterministically tracked. The only constraint * test. This allows tests to be deterministically tracked. The only constraint
* for a permanent identifier is that it must not be blank. * for a permanent identifier is that it must not be blank.
@ -18,18 +17,21 @@ opaque type PermanentId = String
object PermanentId: object PermanentId:
/** /** Instantiate a new [[PermanentId]].
* Instantiate a new [[PermanentId]]. *
* * @param candidate
* @param candidate The candidate string. * The candidate string.
* @return The new [[PermanentId]] instance. * @return
* @throws IllegalArgumentException If the candidate string is blank. * The new [[PermanentId]] instance.
*/ * @throws IllegalArgumentException
* If the candidate string is blank.
*/
def apply(candidate: String): PermanentId = def apply(candidate: String): PermanentId =
if candidate.isBlank() then if candidate.isBlank() then
throw new IllegalArgumentException("Permanent Identifiers must be non-blank.") throw new IllegalArgumentException(
else "Permanent Identifiers must be non-blank."
candidate )
else candidate
given CanEqual[PermanentId, PermanentId] = CanEqual.derived given CanEqual[PermanentId, PermanentId] = CanEqual.derived

View file

@ -2,19 +2,19 @@ package gs.test.v0.definition
import cats.Show import cats.Show
/** /** Opaque type representing tags that may be assigned to a [[Test]].
* Opaque type representing tags that may be assigned to a [[Test]]. */
*/
opaque type Tag = String opaque type Tag = String
object Tag: object Tag:
/** /** Instantiate a new [[Tag]].
* Instantiate a new [[Tag]]. *
* * @param tag
* @param tag The candidate string. * The candidate string.
* @return The new [[Tag]] instance. * @return
*/ * The new [[Tag]] instance.
*/
def apply(tag: String): Tag = tag def apply(tag: String): Tag = tag
given CanEqual[Tag, Tag] = CanEqual.derived given CanEqual[Tag, Tag] = CanEqual.derived

View file

@ -1,18 +1,24 @@
package gs.test.v0.definition package gs.test.v0.definition
import cats.data.EitherT
import cats.Show import cats.Show
import cats.data.EitherT
/** /** Each instance of this class indicates the _definition_ of some test.
* Each instance of this class indicates the _definition_ of some test.
* *
* @param name The display name of the test. Not considered to be unique. * @param name
* @param permanentId The [[PermanentId]] for this test. * The display name of the test. Not considered to be unique.
* @param documentation The documentation for this test. * @param permanentId
* @param tags The list of [[Tag]] applicable to this test. * The [[PermanentId]] for this test.
* @param markers The list of [[Marker]] applicable to this test. * @param documentation
* @param iterations The number of iterations of this test to run. * The documentation for this test.
* @param unitOfWork The function that the test evaluates. * @param tags
* The list of [[Tag]] applicable to this test.
* @param markers
* The list of [[Marker]] applicable to this test.
* @param iterations
* The number of iterations of this test to run.
* @param unitOfWork
* The function that the test evaluates.
*/ */
final class TestDefinition[F[_]]( final class TestDefinition[F[_]](
val name: TestDefinition.Name, val name: TestDefinition.Name,
@ -26,20 +32,20 @@ final class TestDefinition[F[_]](
object TestDefinition: object TestDefinition:
/** /** Opaque type representing names that may be assigned to [[Test]].
* Opaque type representing names that may be assigned to [[Test]]. */
*/
opaque type Name = String opaque type Name = String
object Name: object Name:
/** /** Instantiate a new [[Test.Name]]. This name is not unique, has no
* Instantiate a new [[Test.Name]]. This name is not unique, has no * constraints, and only exists for display purposes.
* constraints, and only exists for display purposes. *
* * @param name
* @param name The candidate string. * The candidate string.
* @return The new [[Test.Name]] instance. * @return
*/ * The new [[Test.Name]] instance.
*/
def apply(name: String): Name = name def apply(name: String): Name = name
given CanEqual[Name, Name] = CanEqual.derived given CanEqual[Name, Name] = CanEqual.derived

View file

@ -1,40 +1,41 @@
package gs.test.v0.definition package gs.test.v0.definition
/** /** Base trait for all failures recognized by gs-test.
* Base trait for all failures recognized by gs-test.
*/ */
sealed trait TestFailure sealed trait TestFailure
object TestFailure: object TestFailure:
/** /** Returned when assertions in this library fail. Assertions understand how
* Returned when assertions in this library fail. Assertions understand how to * to populate these values.
* populate these values. *
* * @param assertionName
* @param assertionName The name of the assertion. * The name of the assertion.
* @param inputs The names and calculated types of each input to the assertion. * @param inputs
* @param message The message produced by the assertion. * The names and calculated types of each input to the assertion.
*/ * @param message
* The message produced by the assertion.
*/
case class AssertionFailed( case class AssertionFailed(
assertionName: String, assertionName: String,
inputs: Map[String, String], inputs: Map[String, String],
message: String message: String
) extends TestFailure ) extends TestFailure
/** /** Return when a test explicitly calls `fail("...")` or some variant thereof.
* Return when a test explicitly calls `fail("...")` or some variant thereof. *
* * @param message
* @param message The failure message provided by the test author. * The failure message provided by the test author.
*/ */
case class TestRequestedFailure( case class TestRequestedFailure(
message: String message: String
) extends TestFailure ) extends TestFailure
/** /** Used when the test fails due to an exception.
* Used when the test fails due to an exception. *
* * @param cause
* @param cause The underlying cause of failure. * The underlying cause of failure.
*/ */
case class ExceptionThrown( case class ExceptionThrown(
cause: Throwable cause: Throwable
) extends TestFailure ) extends TestFailure

View file

@ -1,14 +1,13 @@
package gs.test.v0.definition package gs.test.v0.definition
import cats.syntax.all.*
import cats.effect.Async
import scala.collection.mutable.ListBuffer
import cats.data.EitherT import cats.data.EitherT
import cats.effect.Async
import cats.syntax.all.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters.* import scala.jdk.CollectionConverters.*
/** /** Base class for defining groups of related tests. Users should extend this
* Base class for defining groups of related tests. Users should extend this
* class to define their tests. * class to define their tests.
* *
* ## Example * ## Example
@ -27,38 +26,38 @@ import scala.jdk.CollectionConverters.*
* }}} * }}}
*/ */
abstract class TestGroup[F[_]: Async]: abstract class TestGroup[F[_]: Async]:
/** /** @return
* @return The display name for this group. * The display name for this group.
*/ */
def name: String def name: String
/** /** @return
* @return List of [[Tag]] that apply to all tests within this group. * List of [[Tag]] that apply to all tests within this group.
*/ */
def tags: List[Tag] = List.empty def tags: List[Tag] = List.empty
/** /** @return
* @return List of all [[Marker]] that apply to all tests within this group. * List of all [[Marker]] that apply to all tests within this group.
*/ */
def markers: List[Marker] = List.empty def markers: List[Marker] = List.empty
/** /** @return
* @return The documentation for this group. * The documentation for this group.
*/ */
def documentation: Option[String] = None def documentation: Option[String] = None
private var beforeGroupValue: Option[F[Unit]] = None private var beforeGroupValue: Option[F[Unit]] = None
private var afterGroupValue: Option[F[Unit]] = None private var afterGroupValue: Option[F[Unit]] = None
private var beforeEachTestValue: Option[F[Unit]] = None private var beforeEachTestValue: Option[F[Unit]] = None
private var afterEachTestValue: Option[F[Unit]] = None private var afterEachTestValue: Option[F[Unit]] = None
private val registry: TestGroup.Registry[F] = new TestGroup.Registry[F] private val registry: TestGroup.Registry[F] = new TestGroup.Registry[F]
/** /** Compile the contents of this [[TestGroup]] for delivery to the engine.
* Compile the contents of this [[TestGroup]] for delivery to the engine. *
* * @return
* @return The immutable, compiled form of this [[TestGroup]]. * The immutable, compiled form of this [[TestGroup]].
*/ */
def compile(): TestGroupDefinition[F] = def compile(): TestGroupDefinition[F] =
new TestGroupDefinition[F]( new TestGroupDefinition[F](
name = TestGroupDefinition.Name(name), name = TestGroupDefinition.Name(name),
@ -72,67 +71,69 @@ abstract class TestGroup[F[_]: Async]:
tests = registry.toList() tests = registry.toList()
) )
/** /** Provide an effect that must run before any of the tests within this group
* Provide an effect that must run before any of the tests within this group * are executed.
* are executed. *
* * @param f
* @param f The effect to run. * The effect to run.
*/ */
protected def beforeGroup(f: => F[Unit]): Unit = protected def beforeGroup(f: => F[Unit]): Unit =
beforeGroupValue = Some(f) beforeGroupValue = Some(f)
() ()
/** /** Provide an effect that must run after all tests within this group have
* Provide an effect that must run after all tests within this group have * finished execution.
* finished execution. *
* * @param f
* @param f The effect to run. * The effect to run.
*/ */
protected def afterGroup(f: => F[Unit]): Unit = protected def afterGroup(f: => F[Unit]): Unit =
afterGroupValue = Some(f) afterGroupValue = Some(f)
() ()
/** /** Provide an effect that must run before each test within this group.
* Provide an effect that must run before each test within this group. *
* * @param f
* @param f The effect to run. * The effect to run.
*/ */
protected def beforeEachTest(f: => F[Unit]): Unit = protected def beforeEachTest(f: => F[Unit]): Unit =
beforeEachTestValue = Some(f) beforeEachTestValue = Some(f)
() ()
/** /** Provide an effect that must run after each test within this group.
* Provide an effect that must run after each test within this group. *
* * @param f
* @param f The effect to run. * The effect to run.
*/ */
protected def afterEachTest(f: => F[Unit]): Unit = protected def afterEachTest(f: => F[Unit]): Unit =
afterEachTestValue = Some(f) afterEachTestValue = Some(f)
() ()
/** /** Define a new test.
* Define a new test. *
* * ## Required Information
* ## Required Information *
* * All tests require 3 things, at minimum:
* All tests require 3 things, at minimum: *
* * - [[PermanentId]]
* - [[PermanentId]] * - Display Name
* - Display Name * - Unit of Work (the code to execute)
* - Unit of Work (the code to execute) *
* * The [[PermanentId]] must be unique within the [[TestSuite]].
* The [[PermanentId]] must be unique within the [[TestSuite]]. *
* * ## Default Values
* ## Default Values *
* * Tests iterate 1 time by default. Tags and Markers are inherited from the
* Tests iterate 1 time by default. Tags and Markers are inherited from the * parent group. If this group contains tag "foo", any test within this group
* parent group. If this group contains tag "foo", any test within this group * will also get tag "foo".
* will also get tag "foo". *
* * @param permanentId
* @param permanentId The [[PermanentId]] for this test. * The [[PermanentId]] for this test.
* @param name The display name for this test. * @param name
* @return A builder, to help complete test definition. * The display name for this test.
*/ * @return
* A builder, to help complete test definition.
*/
protected def test( protected def test(
permanentId: PermanentId, permanentId: PermanentId,
name: String name: String
@ -142,93 +143,106 @@ abstract class TestGroup[F[_]: Async]:
name = TestDefinition.Name(name), name = TestDefinition.Name(name),
permanentId = permanentId, permanentId = permanentId,
tags = ListBuffer(tags*), tags = ListBuffer(tags*),
markers = ListBuffer(markers*), markers = ListBuffer(markers*)
) )
object TestGroup: object TestGroup:
/** /** Specialization of [[TestGroup]] for `cats.effect.IO`, the typical use
* Specialization of [[TestGroup]] for `cats.effect.IO`, the typical use case. * case.
*/ */
abstract class IO extends TestGroup[cats.effect.IO] abstract class IO extends TestGroup[cats.effect.IO]
/** /** Builder to assist with defining tests.
* Builder to assist with defining tests. *
* * @param registry
* @param registry Registry instance internal to a [[TestGroup]] for recording completed definitions. * Registry instance internal to a [[TestGroup]] for recording completed
* @param name The name of the test. * definitions.
* @param permanentId The [[PermanentId]] of the test. * @param name
* @param tags List of [[TestDefinition.Tag]] applicable to this test. * The name of the test.
* @param markers List of [[TestDefinition.Marker]] applicable to this test. * @param permanentId
* @param documentation The documentation for this test. * The [[PermanentId]] of the test.
* @param iterations Number of iterations to run this test. * @param tags
*/ * List of [[TestDefinition.Tag]] applicable to this test.
protected final class TestBuilder[F[_]: Async]( * @param markers
* List of [[TestDefinition.Marker]] applicable to this test.
* @param documentation
* The documentation for this test.
* @param iterations
* Number of iterations to run this test.
*/
final protected class TestBuilder[F[_]: Async](
val registry: Registry[F], val registry: Registry[F],
val name: TestDefinition.Name, val name: TestDefinition.Name,
val permanentId: PermanentId, val permanentId: PermanentId,
private val tags: ListBuffer[Tag], private val tags: ListBuffer[Tag],
private val markers: ListBuffer[Marker], private val markers: ListBuffer[Marker],
private var documentation: Option[String] = None, private var documentation: Option[String] = None,
private var iterations: TestIterations = TestIterations.One, private var iterations: TestIterations = TestIterations.One
): ):
/**
* Supply documentation for this test. /** Supply documentation for this test.
* *
* @param docs The documentation for this test. * @param docs
* @return This builder. * The documentation for this test.
*/ * @return
* This builder.
*/
def document(docs: String): TestBuilder[F] = def document(docs: String): TestBuilder[F] =
documentation = Some(docs) documentation = Some(docs)
this this
/** /** Add additional [[Test.Tag]] to this test definition.
* Add additional [[Test.Tag]] to this test definition. *
* * @param additionalTags
* @param additionalTags The list of new tags. * The list of new tags.
* @return This builder. * @return
*/ * This builder.
*/
def tagged(additionalTags: Tag*): TestBuilder[F] = def tagged(additionalTags: Tag*): TestBuilder[F] =
val _ = tags.addAll(additionalTags) val _ = tags.addAll(additionalTags)
this this
/** /** Add the [[TestDefinition.Marker.Ignored]] marker to this test
* Add the [[TestDefinition.Marker.Ignored]] marker to this test definition. * definition.
* *
* @return This builder. * @return
*/ * This builder.
*/
def ignored(): TestBuilder[F] = def ignored(): TestBuilder[F] =
val _ = markers.addOne(Marker.Ignored) val _ = markers.addOne(Marker.Ignored)
this this
/** /** Add one or more [[TestDefinition.Marker]] to this test definition.
* Add one or more [[TestDefinition.Marker]] to this test definition. *
* * @param additionalMarkers
* @param additionalMarkers The list of markers to add. * The list of markers to add.
* @return This builder. * @return
*/ * This builder.
*/
def marked(additionalMarkers: Marker*): TestBuilder[F] = def marked(additionalMarkers: Marker*): TestBuilder[F] =
val _ = markers.addAll(additionalMarkers) val _ = markers.addAll(additionalMarkers)
this this
/** /** Set the number of times this test should iterate.
* Set the number of times this test should iterate. *
* * @param iters
* @param iters The number of iterations. * The number of iterations.
* @return This builder. * @return
*/ * This builder.
*/
def iterate(iters: TestIterations): TestBuilder[F] = def iterate(iters: TestIterations): TestBuilder[F] =
iterations = iters iterations = iters
this this
/** /** Provide an input supplier for this test. Note that each iteration of the
* Provide an input supplier for this test. Note that each iteration of the * test results in the input function being evaluated.
* test results in the input function being evaluated. *
* * @param f
* @param f The input function. * The input function.
* @return Builder that supports input. * @return
*/ * Builder that supports input.
*/
def input[Input](f: F[Input]): InputTestBuilder[F, Input] = def input[Input](f: F[Input]): InputTestBuilder[F, Input] =
new InputTestBuilder[F, Input]( new InputTestBuilder[F, Input](
registry = registry, registry = registry,
@ -240,52 +254,62 @@ object TestGroup:
iterations = iterations iterations = iterations
) )
/** /** Finalize and register this test with a pure unit of work.
* Finalize and register this test with a pure unit of work. *
* * @param unitOfWork
* @param unitOfWork The function this test will execute. * The function this test will execute.
*/ */
def pure(unitOfWork: => Either[TestFailure, Unit]): Unit = def pure(unitOfWork: => Either[TestFailure, Unit]): Unit =
apply(EitherT.fromEither[F](unitOfWork)) apply(EitherT.fromEither[F](unitOfWork))
/** /** Finalize and register this test with an effectful unit of work.
* Finalize and register this test with an effectful unit of work. *
* * @param unitOfWork
* @param unitOfWork The function this test will execute. * The function this test will execute.
*/ */
def effectful(unitOfWork: => F[Either[TestFailure, Unit]]): Unit = def effectful(unitOfWork: => F[Either[TestFailure, Unit]]): Unit =
apply(EitherT(unitOfWork)) apply(EitherT(unitOfWork))
/** /** Finalize and register this test with an effectful unit of work.
* Finalize and register this test with an effectful unit of work. *
* * @param unitOfWork
* @param unitOfWork The function this test will execute. * The function this test will execute.
*/ */
def apply(unitOfWork: => EitherT[F, TestFailure, Unit]): Unit = def apply(unitOfWork: => EitherT[F, TestFailure, Unit]): Unit =
registry.register(new TestDefinition[F]( registry.register(
name = name, new TestDefinition[F](
permanentId = permanentId, name = name,
documentation = documentation, permanentId = permanentId,
tags = tags.distinct.toList, documentation = documentation,
markers = markers.distinct.toList, tags = tags.distinct.toList,
iterations = iterations, markers = markers.distinct.toList,
unitOfWork = unitOfWork iterations = iterations,
)) unitOfWork = unitOfWork
)
)
/** /** Builder to assist with defining tests. This builder is for tests which
* Builder to assist with defining tests. This builder is for tests which * accept input via some producing function.
* accept input via some producing function. *
* * @param registry
* @param registry Registry instance internal to a [[TestGroup]] for recording completed definitions. * Registry instance internal to a [[TestGroup]] for recording completed
* @param name The name of the test. * definitions.
* @param permanentId The [[PermanentId]] of the test. * @param name
* @param inputFunction The function that provides input to this test. * The name of the test.
* @param tags List of [[TestDefinition.Tag]] applicable to this test. * @param permanentId
* @param markers List of [[TestDefinition.Marker]] applicable to this test. * The [[PermanentId]] of the test.
* @param documentation The documentation for this test. * @param inputFunction
* @param iterations Number of iterations to run this test. * The function that provides input to this test.
*/ * @param tags
protected final class InputTestBuilder[F[_]: Async, Input]( * List of [[TestDefinition.Tag]] applicable to this test.
* @param markers
* List of [[TestDefinition.Marker]] applicable to this test.
* @param documentation
* The documentation for this test.
* @param iterations
* Number of iterations to run this test.
*/
final protected class InputTestBuilder[F[_]: Async, Input](
val registry: Registry[F], val registry: Registry[F],
val name: TestDefinition.Name, val name: TestDefinition.Name,
val permanentId: PermanentId, val permanentId: PermanentId,
@ -293,90 +317,99 @@ object TestGroup:
private val tags: ListBuffer[Tag], private val tags: ListBuffer[Tag],
private val markers: ListBuffer[Marker], private val markers: ListBuffer[Marker],
private var documentation: Option[String] = None, private var documentation: Option[String] = None,
private var iterations: TestIterations = TestIterations.One, private var iterations: TestIterations = TestIterations.One
): ):
/**
* Supply documentation for this test. /** Supply documentation for this test.
* *
* @param docs The documentation for this test. * @param docs
* @return This builder. * The documentation for this test.
*/ * @return
* This builder.
*/
def document(docs: String): InputTestBuilder[F, Input] = def document(docs: String): InputTestBuilder[F, Input] =
documentation = Some(docs) documentation = Some(docs)
this this
/** /** Add additional [[Test.Tag]] to this test definition.
* Add additional [[Test.Tag]] to this test definition. *
* * @param additionalTags
* @param additionalTags The list of new tags. * The list of new tags.
* @return This builder. * @return
*/ * This builder.
*/
def tagged(additionalTags: Tag*): InputTestBuilder[F, Input] = def tagged(additionalTags: Tag*): InputTestBuilder[F, Input] =
val _ = tags.addAll(additionalTags) val _ = tags.addAll(additionalTags)
this this
/** /** Add the [[TestDefinition.Marker.Ignored]] marker to this test
* Add the [[TestDefinition.Marker.Ignored]] marker to this test definition. * definition.
* *
* @return This builder. * @return
*/ * This builder.
*/
def ignored(): InputTestBuilder[F, Input] = def ignored(): InputTestBuilder[F, Input] =
val _ = markers.addOne(Marker.Ignored) val _ = markers.addOne(Marker.Ignored)
this this
/** /** Add one or more [[TestDefinition.Marker]] to this test definition.
* Add one or more [[TestDefinition.Marker]] to this test definition. *
* * @param additionalMarkers
* @param additionalMarkers The list of markers to add. * The list of markers to add.
* @return This builder. * @return
*/ * This builder.
*/
def marked(additionalMarkers: Marker*): InputTestBuilder[F, Input] = def marked(additionalMarkers: Marker*): InputTestBuilder[F, Input] =
val _ = markers.addAll(additionalMarkers) val _ = markers.addAll(additionalMarkers)
this this
/** /** Set the number of times this test should iterate.
* Set the number of times this test should iterate. *
* * @param iters
* @param iters The number of iterations. * The number of iterations.
* @return This builder. * @return
*/ * This builder.
*/
def iterate(iters: TestIterations): InputTestBuilder[F, Input] = def iterate(iters: TestIterations): InputTestBuilder[F, Input] =
iterations = iters iterations = iters
this this
/** /** Finalize and register this test with a pure unit of work.
* Finalize and register this test with a pure unit of work. *
* * @param unitOfWork
* @param unitOfWork The function this test will execute. * The function this test will execute.
*/ */
def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit = def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit =
apply(input => EitherT(Async[F].delay(unitOfWork(input)))) apply(input => EitherT(Async[F].delay(unitOfWork(input))))
/** /** Finalize and register this test with an effectful unit of work.
* Finalize and register this test with an effectful unit of work. *
* * @param unitOfWork
* @param unitOfWork The function this test will execute. * The function this test will execute.
*/ */
def effectful(unitOfWork: Input => F[Either[TestFailure, Unit]]): Unit = def effectful(unitOfWork: Input => F[Either[TestFailure, Unit]]): Unit =
apply(input => EitherT(unitOfWork(input))) apply(input => EitherT(unitOfWork(input)))
/** /** Finalize and register this test with an effectful unit of work.
* Finalize and register this test with an effectful unit of work. *
* * @param unitOfWork
* @param unitOfWork The function this test will execute. * The function this test will execute.
*/ */
def apply(unitOfWork: Input => EitherT[F, TestFailure, Unit]): Unit = def apply(unitOfWork: Input => EitherT[F, TestFailure, Unit]): Unit =
registry.register(new TestDefinition[F]( registry.register(
name = name, new TestDefinition[F](
permanentId = permanentId, name = name,
documentation = documentation, permanentId = permanentId,
tags = tags.distinct.toList, documentation = documentation,
markers = markers.distinct.toList, tags = tags.distinct.toList,
iterations = iterations, markers = markers.distinct.toList,
unitOfWork = EitherT.right(inputFunction).flatMap(unitOfWork) iterations = iterations,
)) unitOfWork = EitherT.right(inputFunction).flatMap(unitOfWork)
)
)
final protected class Registry[F[_]]:
protected final class Registry[F[_]]:
val mapping: ConcurrentHashMap[PermanentId, TestDefinition[F]] = val mapping: ConcurrentHashMap[PermanentId, TestDefinition[F]] =
new ConcurrentHashMap[PermanentId, TestDefinition[F]] new ConcurrentHashMap[PermanentId, TestDefinition[F]]
@ -385,8 +418,7 @@ object TestGroup:
throw new IllegalArgumentException( throw new IllegalArgumentException(
s"Attempted to register test with duplicate Permanent ID '${test.permanentId.show}'." s"Attempted to register test with duplicate Permanent ID '${test.permanentId.show}'."
) )
else else mapping.put(test.permanentId, test)
mapping.put(test.permanentId, test)
def toList(): List[TestDefinition[F]] = mapping.values().asScala.toList def toList(): List[TestDefinition[F]] = mapping.values().asScala.toList

View file

@ -2,16 +2,20 @@ package gs.test.v0.definition
import cats.Show import cats.Show
/** /** Each group is comprised of a list of [[Test]]. This list may be empty.
* Each group is comprised of a list of [[Test]]. This list may be empty.
* *
* Groups are essentially metadata for tests for viewing/organization purposes. * Groups are essentially metadata for tests for viewing/organization purposes.
* *
* @param name The group name. Not considered to be unique. * @param name
* @param documentation Arbitrary documentation for this group of tests. * The group name. Not considered to be unique.
* @param testTags Set of tags applied to all [[TestDefinition]] within the group. * @param documentation
* @param testMarkers Set of markers applied to all [[TestDefinition]] within the group. * Arbitrary documentation for this group of tests.
* @param tests The list of tests in this group. * @param testTags
* Set of tags applied to all [[TestDefinition]] within the group.
* @param testMarkers
* Set of markers applied to all [[TestDefinition]] within the group.
* @param tests
* The list of tests in this group.
*/ */
final class TestGroupDefinition[F[_]]( final class TestGroupDefinition[F[_]](
val name: TestGroupDefinition.Name, val name: TestGroupDefinition.Name,
@ -27,20 +31,20 @@ final class TestGroupDefinition[F[_]](
object TestGroupDefinition: object TestGroupDefinition:
/** /** Opaque type representing names that may be assigned to test groups.
* Opaque type representing names that may be assigned to test groups. */
*/
opaque type Name = String opaque type Name = String
object Name: object Name:
/** /** Instantiate a new [[TestGroup.Name]]. This name is not unique, has no
* Instantiate a new [[TestGroup.Name]]. This name is not unique, has no * constraints, and only exists for display purposes.
* constraints, and only exists for display purposes. *
* * @param name
* @param name The candidate string. * The candidate string.
* @return The new [[TestGroup.Name]] instance. * @return
*/ * The new [[TestGroup.Name]] instance.
*/
def apply(name: String): Name = name def apply(name: String): Name = name
given CanEqual[Name, Name] = CanEqual.derived given CanEqual[Name, Name] = CanEqual.derived

View file

@ -2,36 +2,34 @@ package gs.test.v0.definition
import cats.Show import cats.Show
/** /** Opaque type that represents the number of iterations a test should run. This
* Opaque type that represents the number of iterations a test should run. * value must be at least `1` (the default). To ignore a test, use the
* This value must be at least `1` (the default). To ignore a test, use the * [[Test.Marker.Ignored]] marker.
* [[Test.Marker.Ignored]] marker. */
*/
opaque type TestIterations = Int opaque type TestIterations = Int
object TestIterations: object TestIterations:
def One: TestIterations = 1 def One: TestIterations = 1
/** /** Validate and instantiate a new [[TestIterations]] instance.
* Validate and instantiate a new [[TestIterations]] instance. *
* * @param candidate
* @param candidate The candidate value. Must be 1 or greater. * The candidate value. Must be 1 or greater.
* @return The new [[TestIterations]], or an error if an invalid input is given. * @return
*/ * The new [[TestIterations]], or an error if an invalid input is given.
*/
def apply(candidate: Int): TestIterations = def apply(candidate: Int): TestIterations =
if candidate < 1 then if candidate < 1 then
throw new IllegalArgumentException( throw new IllegalArgumentException(
s"Tests must iterate at least once. Received candidate '$candidate'." s"Tests must iterate at least once. Received candidate '$candidate'."
) )
else else candidate
candidate
given CanEqual[TestIterations, TestIterations] = CanEqual.derived given CanEqual[TestIterations, TestIterations] = CanEqual.derived
given Show[TestIterations] = iters => iters.toString() given Show[TestIterations] = iters => iters.toString()
extension (iters: TestIterations) extension (iters: TestIterations) def toInt(): Int = iters
def toInt(): Int = iters
end TestIterations end TestIterations

View file

@ -1,16 +1,18 @@
package gs.test.v0.definition package gs.test.v0.definition
/** /** The Test Suite is the primary unit of organization within `gs-test` -- each
* The Test Suite is the primary unit of organization within `gs-test` -- each
* execution _typically_ runs a single test suite. For example, the unit tests * execution _typically_ runs a single test suite. For example, the unit tests
* for some project would likely comprise of a single suite. * for some project would likely comprise of a single suite.
* *
* Within each suite is a list of [[TestGroup]], arbitrary ways to organize * Within each suite is a list of [[TestGroup]], arbitrary ways to organize
* individual [[Test]] definitions. * individual [[Test]] definitions.
* *
* @param name The name of this test suite. * @param name
* @param documentation Arbitrary documentation for this suite of tests. * The name of this test suite.
* @param groups List of [[TestGroup]] owned by this suite. * @param documentation
* Arbitrary documentation for this suite of tests.
* @param groups
* List of [[TestGroup]] owned by this suite.
*/ */
case class TestSuite[F[_]]( case class TestSuite[F[_]](
name: String, name: String,

View file

@ -5,8 +5,7 @@ import cats.data.EitherT
import cats.effect.Sync import cats.effect.Sync
import cats.syntax.all.* import cats.syntax.all.*
/** /** String interpolator for [[Tag]]. Shorthand for producing new [[Tag]]
* String interpolator for [[Tag]]. Shorthand for producing new [[Tag]]
* instances. * instances.
* *
* {{{ * {{{
@ -14,11 +13,9 @@ import cats.syntax.all.*
* val tag1: TestDefinition.Tag = tag"example" * val tag1: TestDefinition.Tag = tag"example"
* }}} * }}}
*/ */
extension (sc: StringContext) extension (sc: StringContext) def tag(args: Any*): Tag = Tag(sc.s(args*))
def tag(args: Any*): Tag = Tag(sc.s(args*))
/** /** String interpolator for [[PermanentId]]. Shorthand for producing new
* String interpolator for [[PermanentId]]. Shorthand for producing new
* [[PermanentId]] instances. * [[PermanentId]] instances.
* *
* {{{ * {{{
@ -29,35 +26,37 @@ extension (sc: StringContext)
extension (sc: StringContext) extension (sc: StringContext)
def pid(args: Any*): PermanentId = PermanentId(sc.s(args*)) def pid(args: Any*): PermanentId = PermanentId(sc.s(args*))
/** /** Request this test to fail (pure form).
* Request this test to fail (pure form).
* *
* @param message The message to report - why did this test fail? * @param message
* @return The failing test result. * The message to report - why did this test fail?
* @return
* The failing test result.
*/ */
def fail(message: String): Either[TestFailure, Unit] = def fail(message: String): Either[TestFailure, Unit] =
Left(TestFailure.TestRequestedFailure(message)) Left(TestFailure.TestRequestedFailure(message))
/** /** Request this test to fail (lifted into F).
* Request this test to fail (lifted into F).
* *
* @param message The message to report - why did this test fail? * @param message
* @return The failing test result. * The message to report - why did this test fail?
* @return
* The failing test result.
*/ */
def failF[F[_]: Applicative](message: String): F[Either[TestFailure, Unit]] = def failF[F[_]: Applicative](message: String): F[Either[TestFailure, Unit]] =
Applicative[F].pure(fail(message)) Applicative[F].pure(fail(message))
/** /** Request this test to fail (lifted into EitherT).
* Request this test to fail (lifted into EitherT).
* *
* @param message The message to report - why did this test fail? * @param message
* @return The failing test result. * The message to report - why did this test fail?
* @return
* The failing test result.
*/ */
def failT[F[_]: Applicative](message: String): EitherT[F, TestFailure, Unit] = def failT[F[_]: Applicative](message: String): EitherT[F, TestFailure, Unit] =
EitherT(failF(message)) EitherT(failF(message))
/** /** Shorthand for indicating a passing test (pure form).
* Shorthand for indicating a passing test (pure form).
* *
* ## Example * ## Example
* *
@ -70,12 +69,12 @@ def failT[F[_]: Applicative](message: String): EitherT[F, TestFailure, Unit] =
* test(pid"ex", "Example Test").pure { pass() } * test(pid"ex", "Example Test").pure { pass() }
* }}} * }}}
* *
* @return The passing test result. * @return
* The passing test result.
*/ */
def pass(): Either[TestFailure, Unit] = Right(()) def pass(): Either[TestFailure, Unit] = Right(())
/** /** Shorthand for indicating a passing test (lifted into F).
* Shorthand for indicating a passing test (lifted into F).
* *
* ## Example * ## Example
* *
@ -88,13 +87,13 @@ def pass(): Either[TestFailure, Unit] = Right(())
* test(pid"ex", "Example Test").effectful { passF() } * test(pid"ex", "Example Test").effectful { passF() }
* }}} * }}}
* *
* @return The passing test result. * @return
* The passing test result.
*/ */
def passF[F[_]: Applicative](): F[Either[TestFailure, Unit]] = def passF[F[_]: Applicative](): F[Either[TestFailure, Unit]] =
Applicative[F].pure(Right(())) Applicative[F].pure(Right(()))
/** /** Shorthand for indicating a passing test (lifted into EitherT).
* Shorthand for indicating a passing test (lifted into EitherT).
* *
* ## Example * ## Example
* *
@ -107,35 +106,45 @@ def passF[F[_]: Applicative](): F[Either[TestFailure, Unit]] =
* test(pid"ex", "Example Test") { passT() } * test(pid"ex", "Example Test") { passT() }
* }}} * }}}
* *
* @return The passing test result. * @return
* The passing test result.
*/ */
def passT[F[_]: Applicative](): EitherT[F, TestFailure, Unit] = def passT[F[_]: Applicative](): EitherT[F, TestFailure, Unit] =
EitherT(passF()) EitherT(passF())
/** /** Check all of the given results, returning the first failure, or a successful
* Check all of the given results, returning the first failure, or a successful
* result if no result failed. * result if no result failed.
* *
* @param results The list of results to check. * @param results
* @return Successful result or the first failure. * The list of results to check.
* @return
* Successful result or the first failure.
*/ */
def checkAll( def checkAll(
results: Either[TestFailure, Unit]* results: Either[TestFailure, Unit]*
): Either[TestFailure, Unit] = ): Either[TestFailure, Unit] =
val initial: Either[TestFailure, Unit] = Right(()) val initial: Either[TestFailure, Unit] = Right(())
results.foldLeft(initial) { (acc, result) => results.foldLeft(initial) {
acc match (
case Left(_) => acc acc,
case Right(_) => result result
) =>
acc match
case Left(_) => acc
case Right(_) => result
} }
def checkAllF[F[_]: Sync]( def checkAllF[F[_]: Sync](
checks: F[Either[TestFailure, Unit]]* checks: F[Either[TestFailure, Unit]]*
): F[Either[TestFailure, Unit]] = ): F[Either[TestFailure, Unit]] =
val initial: F[Either[TestFailure, Unit]] = Sync[F].delay(Right(())) val initial: F[Either[TestFailure, Unit]] = Sync[F].delay(Right(()))
checks.foldLeft(initial) { (acc, result) => checks.foldLeft(initial) {
acc.flatMap { (
case Right(_) => result acc,
case err => Sync[F].pure(err) result
} ) =>
acc.flatMap {
case Right(_) => result
case err => Sync[F].pure(err)
}
} }

View file

@ -1,15 +1,14 @@
package gs.test.v0.definition package gs.test.v0.definition
import munit.*
import cats.effect.IO import cats.effect.IO
import gs.test.v0.definition.{Tag => GsTag} import gs.test.v0.definition.{Tag => GsTag}
import munit.*
class GroupImplementationTests extends FunSuite: class GroupImplementationTests extends FunSuite:
import GroupImplementationTests.* import GroupImplementationTests.*
test("should support a group with a simple, pure, test") { test("should support a group with a simple, pure, test") {
val g1 = new G1 val g1 = new G1
val group = g1.compile() val group = g1.compile()
assertEquals(group.name, TestGroupDefinition.Name("G1")) assertEquals(group.name, TestGroupDefinition.Name("G1"))
assertEquals(group.documentation, None) assertEquals(group.documentation, None)
@ -33,7 +32,7 @@ class GroupImplementationTests extends FunSuite:
} }
test("should support a group with all values set") { test("should support a group with all values set") {
val g2 = new G2 val g2 = new G2
val group = g2.compile() val group = g2.compile()
assertEquals(group.name, TestGroupDefinition.Name("G2")) assertEquals(group.name, TestGroupDefinition.Name("G2"))
assertEquals(group.documentation, Some("docs")) assertEquals(group.documentation, Some("docs"))
@ -57,7 +56,7 @@ class GroupImplementationTests extends FunSuite:
} }
test("should support a simple group with a configured test") { test("should support a simple group with a configured test") {
val g3 = new G3 val g3 = new G3
val group = g3.compile() val group = g3.compile()
assertEquals(group.name, TestGroupDefinition.Name("G3")) assertEquals(group.name, TestGroupDefinition.Name("G3"))
assertEquals(group.documentation, None) assertEquals(group.documentation, None)
@ -92,10 +91,11 @@ object GroupImplementationTests:
class G1 extends TestGroup.IO: class G1 extends TestGroup.IO:
override def name: String = "G1" override def name: String = "G1"
test(Ids.T1, "simple").pure { Right(()) } test(Ids.T1, "simple").pure(Right(()))
end G1 end G1
class G2 extends TestGroup.IO: class G2 extends TestGroup.IO:
override def name: String = override def name: String =
"G2" "G2"
@ -108,12 +108,12 @@ object GroupImplementationTests:
override def markers: List[Marker] = override def markers: List[Marker] =
List(Marker.Ignored) List(Marker.Ignored)
beforeGroup { IO.unit } beforeGroup(IO.unit)
afterGroup { IO.unit } afterGroup(IO.unit)
beforeEachTest { IO.unit } beforeEachTest(IO.unit)
afterEachTest { IO.unit } afterEachTest(IO.unit)
test(Ids.T2, "inherit from group").pure { Right(()) } test(Ids.T2, "inherit from group").pure(Right(()))
end G2 end G2
class G3 extends TestGroup.IO: class G3 extends TestGroup.IO:
@ -124,7 +124,8 @@ object GroupImplementationTests:
.tagged(tag"tag1", tag"tag2") .tagged(tag"tag1", tag"tag2")
.marked(Marker.Ignored) .marked(Marker.Ignored)
.iterate(TestIterations(2)) .iterate(TestIterations(2))
.pure { Right(()) } .pure(Right(()))
end G3 end G3
end GroupImplementationTests end GroupImplementationTests