From 79e6672bdf011f22231edc076f32c5290276006d Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Tue, 3 Sep 2024 22:05:51 -0500 Subject: [PATCH] Pre-commit. --- README.md | 2 +- build.sbt | 36 +- .../gs/test/v0/definition/Assertion.scala | 48 +- .../scala/gs/test/v0/definition/Check.scala | 21 +- .../scala/gs/test/v0/definition/Marker.scala | 21 +- .../gs/test/v0/definition/PermanentId.scala | 30 +- .../scala/gs/test/v0/definition/Tag.scala | 18 +- .../test/v0/definition/TestDefinition.scala | 46 +- .../gs/test/v0/definition/TestFailure.scala | 41 +- .../gs/test/v0/definition/TestGroup.scala | 490 ++++++++++-------- .../v0/definition/TestGroupDefinition.scala | 38 +- .../test/v0/definition/TestIterations.scala | 30 +- .../gs/test/v0/definition/TestSuite.scala | 12 +- .../scala/gs/test/v0/definition/syntax.scala | 91 ++-- .../gs/test/v0/GroupImplementationTests.scala | 35 +- 15 files changed, 519 insertions(+), 440 deletions(-) diff --git a/README.md b/README.md index 236855c..9ee62b2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [GS Open Source](https://garrity.co/oss.html) | [License (MIT)](./LICENSE) -Test framework for Scala 3. Based on +Test framework for Scala 3. Based on [Cats Effect](https://typelevel.org/cats-effect/) and [FS2](https://fs2.io/#/). diff --git a/build.sbt b/build.sbt index 00447f3..ba8ed64 100644 --- a/build.sbt +++ b/build.sbt @@ -35,7 +35,7 @@ val Deps = 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" } @@ -44,7 +44,7 @@ val Deps = new { lazy val testSettings = Seq( libraryDependencies ++= Seq( - Deps.MUnit % Test, + Deps.MUnit % Test, Deps.Gs.Datagen % Test ) ) @@ -62,20 +62,28 @@ lazy val `api-definition` = project .in(file("modules/api-definition")) .settings(sharedSettings) .settings(testSettings) - .settings(name := s"${gsProjectName.value}-api-definition-v${semVerMajor.value}") - .settings(libraryDependencies ++= Seq( - Deps.Cats.Core, - Deps.Cats.Effect, - Deps.Fs2.Core - )) + .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, - Deps.Fs2.Core - )) + .settings( + name := s"${gsProjectName.value}-api-execution-v${semVerMajor.value}" + ) + .settings( + libraryDependencies ++= Seq( + Deps.Cats.Core, + Deps.Cats.Effect, + Deps.Fs2.Core + ) + ) diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/Assertion.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/Assertion.scala index 5e46ad0..db8a16f 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/Assertion.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/Assertion.scala @@ -13,32 +13,40 @@ object Assertion: // TODO: Code Position case object IsEqualTo extends Assertion("isEqualTo"): + def evaluate[A: ClassTag]( - candidate: A, + candidate: A, expected: A - )(using CanEqual[A, A]): Either[TestFailure, Unit] = - if candidate == expected then - success() + )( + 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)}'" - )) + 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'." - )) + if candidate then success() + else + Left( + TestFailure.AssertionFailed( + assertionName = name, + inputs = Map("candidate" -> "Boolean"), + message = s"Expected '$candidate' to be 'true'." + ) + ) end Assertion diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/Check.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/Check.scala index 037db73..4ff9398 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/Check.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/Check.scala @@ -1,7 +1,7 @@ package gs.test.v0.definition -import scala.reflect.ClassTag import cats.effect.Sync +import scala.reflect.ClassTag opaque type Check[A] = A @@ -12,20 +12,29 @@ object Check: def apply[A](candidate: A): Check[A] = candidate extension [A: ClassTag](check: Check[A]) - /** - * @return The unwrapped value of this [[Check]]. - */ + /** @return + * The unwrapped value of this [[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) def isEqualToF[F[_]: Sync]( expected: A - )(using CanEqual[A, A]): F[TestResult] = + )( + using + CanEqual[A, A] + ): F[TestResult] = Sync[F].delay(isEqualTo(expected)) extension (check: Check[Boolean]) + def isTrue(): TestResult = Assertion.IsTrue.evaluate(check) diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/Marker.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/Marker.scala index d2c841f..f80104c 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/Marker.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/Marker.scala @@ -1,22 +1,21 @@ package gs.test.v0.definition -/** - * Enumeration for _Markers_, special tokens which "mark" a test to change - * execution functionality. - * - * The basic case for this enumeration is allowing tests to be ignored. - * - * @param name The formal serialized name of the marker. - */ +/** Enumeration for _Markers_, special tokens which "mark" a test to change + * execution functionality. + * + * The basic case for this enumeration is allowing tests to be ignored. + * + * @param name + * The formal serialized name of the marker. + */ sealed abstract class Marker(val name: String) object Marker: 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") end Marker diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/PermanentId.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/PermanentId.scala index b972a4f..cffc197 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/PermanentId.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/PermanentId.scala @@ -2,15 +2,14 @@ package gs.test.v0.definition import cats.Show -/** - * Opaque type representing some _permanent identifier_. These are - * user-assigned strings that are expected to _not change over time_ for some +/** Opaque type representing some _permanent identifier_. These are + * user-assigned strings that are expected to _not change over time_ for some * test. This allows tests to be deterministically tracked. The only constraint * for a permanent identifier is that it must not be blank. * * ## Uniqueness * - * Permanent identifiers are expected to be unique within the scope of a + * Permanent identifiers are expected to be unique within the scope of a * [[TestSuite]]. This means that two groups within the same suite _may not_ * contain tests that share a permanent identifier. */ @@ -18,18 +17,21 @@ opaque type PermanentId = String object PermanentId: - /** - * Instantiate a new [[PermanentId]]. - * - * @param candidate The candidate string. - * @return The new [[PermanentId]] instance. - * @throws IllegalArgumentException If the candidate string is blank. - */ + /** Instantiate a new [[PermanentId]]. + * + * @param candidate + * The candidate string. + * @return + * The new [[PermanentId]] instance. + * @throws IllegalArgumentException + * If the candidate string is blank. + */ def apply(candidate: String): PermanentId = if candidate.isBlank() then - throw new IllegalArgumentException("Permanent Identifiers must be non-blank.") - else - candidate + throw new IllegalArgumentException( + "Permanent Identifiers must be non-blank." + ) + else candidate given CanEqual[PermanentId, PermanentId] = CanEqual.derived diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/Tag.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/Tag.scala index ab391af..ee232b4 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/Tag.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/Tag.scala @@ -2,19 +2,19 @@ package gs.test.v0.definition 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 object Tag: - /** - * Instantiate a new [[Tag]]. - * - * @param tag The candidate string. - * @return The new [[Tag]] instance. - */ + /** Instantiate a new [[Tag]]. + * + * @param tag + * The candidate string. + * @return + * The new [[Tag]] instance. + */ def apply(tag: String): Tag = tag given CanEqual[Tag, Tag] = CanEqual.derived diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala index 0b3c088..f04e6b9 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestDefinition.scala @@ -1,18 +1,24 @@ package gs.test.v0.definition -import cats.data.EitherT 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 permanentId The [[PermanentId]] for this test. - * @param documentation The documentation for this test. - * @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. + * @param name + * The display name of the test. Not considered to be unique. + * @param permanentId + * The [[PermanentId]] for this test. + * @param documentation + * The documentation for this test. + * @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[_]]( val name: TestDefinition.Name, @@ -26,20 +32,20 @@ final class TestDefinition[F[_]]( 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 object Name: - /** - * Instantiate a new [[Test.Name]]. This name is not unique, has no - * constraints, and only exists for display purposes. - * - * @param name The candidate string. - * @return The new [[Test.Name]] instance. - */ + /** Instantiate a new [[Test.Name]]. This name is not unique, has no + * constraints, and only exists for display purposes. + * + * @param name + * The candidate string. + * @return + * The new [[Test.Name]] instance. + */ def apply(name: String): Name = name given CanEqual[Name, Name] = CanEqual.derived diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestFailure.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestFailure.scala index 5b5fde6..a2d9a2d 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestFailure.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestFailure.scala @@ -1,40 +1,41 @@ 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 object TestFailure: - /** - * Returned when assertions in this library fail. Assertions understand how to - * populate these values. - * - * @param assertionName The name of the assertion. - * @param inputs The names and calculated types of each input to the assertion. - * @param message The message produced by the assertion. - */ + /** Returned when assertions in this library fail. Assertions understand how + * to populate these values. + * + * @param assertionName + * The name of the assertion. + * @param inputs + * The names and calculated types of each input to the assertion. + * @param message + * The message produced by the assertion. + */ case class AssertionFailed( assertionName: String, inputs: Map[String, String], message: String ) extends TestFailure - /** - * Return when a test explicitly calls `fail("...")` or some variant thereof. - * - * @param message The failure message provided by the test author. - */ + /** Return when a test explicitly calls `fail("...")` or some variant thereof. + * + * @param message + * The failure message provided by the test author. + */ case class TestRequestedFailure( message: String ) extends TestFailure - /** - * Used when the test fails due to an exception. - * - * @param cause The underlying cause of failure. - */ + /** Used when the test fails due to an exception. + * + * @param cause + * The underlying cause of failure. + */ case class ExceptionThrown( cause: Throwable ) extends TestFailure diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala index fff50b0..821869a 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroup.scala @@ -1,14 +1,13 @@ package gs.test.v0.definition -import cats.syntax.all.* -import cats.effect.Async -import scala.collection.mutable.ListBuffer import cats.data.EitherT +import cats.effect.Async +import cats.syntax.all.* import java.util.concurrent.ConcurrentHashMap +import scala.collection.mutable.ListBuffer 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. * * ## Example @@ -27,38 +26,38 @@ import scala.jdk.CollectionConverters.* * }}} */ abstract class TestGroup[F[_]: Async]: - /** - * @return The display name for this group. - */ + /** @return + * The display name for this group. + */ def name: String - /** - * @return List of [[Tag]] that apply to all tests within this group. - */ + /** @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. - */ + /** @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. - */ + /** @return + * The documentation for this group. + */ def documentation: Option[String] = None - private var beforeGroupValue: Option[F[Unit]] = None - private var afterGroupValue: Option[F[Unit]] = None + private var beforeGroupValue: Option[F[Unit]] = None + private var afterGroupValue: 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] - /** - * Compile the contents of this [[TestGroup]] for delivery to the engine. - * - * @return The immutable, compiled form of this [[TestGroup]]. - */ + /** 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), @@ -72,163 +71,178 @@ 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 = + /** 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. - */ + /** 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. - */ + /** 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. - */ + /** 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) () - /** - * Define a new test. - * - * ## Required Information - * - * All tests require 3 things, at minimum: - * - * - [[PermanentId]] - * - Display Name - * - Unit of Work (the code to execute) - * - * The [[PermanentId]] must be unique within the [[TestSuite]]. - * - * ## Default Values - * - * 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 - * will also get tag "foo". - * - * @param permanentId The [[PermanentId]] for this test. - * @param name The display name for this test. - * @return A builder, to help complete test definition. - */ + /** Define a new test. + * + * ## Required Information + * + * All tests require 3 things, at minimum: + * + * - [[PermanentId]] + * - Display Name + * - Unit of Work (the code to execute) + * + * The [[PermanentId]] must be unique within the [[TestSuite]]. + * + * ## Default Values + * + * 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 + * will also get tag "foo". + * + * @param permanentId + * The [[PermanentId]] for this test. + * @param name + * The display name for this test. + * @return + * A builder, to help complete test definition. + */ protected def test( - permanentId: PermanentId, + permanentId: PermanentId, name: String - ): TestGroup.TestBuilder[F] = + ): TestGroup.TestBuilder[F] = new TestGroup.TestBuilder[F]( registry = registry, name = TestDefinition.Name(name), permanentId = permanentId, tags = ListBuffer(tags*), - markers = ListBuffer(markers*), + markers = ListBuffer(markers*) ) - object TestGroup: - /** - * Specialization of [[TestGroup]] for `cats.effect.IO`, the typical use case. - */ + /** Specialization of [[TestGroup]] for `cats.effect.IO`, the typical use + * case. + */ abstract class IO extends TestGroup[cats.effect.IO] - /** - * Builder to assist with defining tests. - * - * @param registry Registry instance internal to a [[TestGroup]] for recording completed definitions. - * @param name The name of the test. - * @param permanentId The [[PermanentId]] of the test. - * @param tags 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. - */ - protected final class TestBuilder[F[_]: Async]( + /** Builder to assist with defining tests. + * + * @param registry + * Registry instance internal to a [[TestGroup]] for recording completed + * definitions. + * @param name + * The name of the test. + * @param permanentId + * The [[PermanentId]] of the test. + * @param tags + * 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 TestBuilder[F[_]: Async]( val registry: Registry[F], val name: TestDefinition.Name, val permanentId: PermanentId, private val tags: ListBuffer[Tag], private val markers: ListBuffer[Marker], private var documentation: Option[String] = None, - private var iterations: TestIterations = TestIterations.One, + private var iterations: TestIterations = TestIterations.One ): - /** - * Supply documentation for this test. - * - * @param docs The documentation for this test. - * @return This builder. - */ + + /** Supply documentation for this test. + * + * @param docs + * The documentation for this test. + * @return + * This builder. + */ def document(docs: String): TestBuilder[F] = documentation = Some(docs) this - /** - * Add additional [[Test.Tag]] to this test definition. - * - * @param additionalTags The list of new tags. - * @return This builder. - */ - def tagged(additionalTags: Tag*): TestBuilder[F] = + /** Add additional [[Test.Tag]] to this test definition. + * + * @param additionalTags + * The list of new tags. + * @return + * This builder. + */ + def tagged(additionalTags: Tag*): TestBuilder[F] = val _ = tags.addAll(additionalTags) this - /** - * Add the [[TestDefinition.Marker.Ignored]] marker to this test definition. - * - * @return This builder. - */ - def ignored(): TestBuilder[F] = + /** Add the [[TestDefinition.Marker.Ignored]] marker to this test + * definition. + * + * @return + * This builder. + */ + def ignored(): TestBuilder[F] = val _ = markers.addOne(Marker.Ignored) this - /** - * Add one or more [[TestDefinition.Marker]] to this test definition. - * - * @param additionalMarkers The list of markers to add. - * @return This builder. - */ - def marked(additionalMarkers: Marker*): TestBuilder[F] = + /** Add one or more [[TestDefinition.Marker]] to this test definition. + * + * @param additionalMarkers + * The list of markers to add. + * @return + * This builder. + */ + def marked(additionalMarkers: Marker*): TestBuilder[F] = val _ = markers.addAll(additionalMarkers) this - /** - * Set the number of times this test should iterate. - * - * @param iters The number of iterations. - * @return This builder. - */ + /** Set the number of times this test should iterate. + * + * @param iters + * The number of iterations. + * @return + * This builder. + */ def iterate(iters: TestIterations): TestBuilder[F] = iterations = iters this - /** - * Provide an input supplier for this test. Note that each iteration of the - * test results in the input function being evaluated. - * - * @param f The input function. - * @return Builder that supports input. - */ + /** Provide an input supplier for this test. Note that each iteration of the + * test results in the input function being evaluated. + * + * @param f + * The input function. + * @return + * Builder that supports input. + */ def input[Input](f: F[Input]): InputTestBuilder[F, Input] = new InputTestBuilder[F, Input]( registry = registry, @@ -240,52 +254,62 @@ object TestGroup: iterations = iterations ) - /** - * Finalize and register this test with a pure unit of work. - * - * @param unitOfWork The function this test will execute. - */ + /** Finalize and register this test with a pure unit of work. + * + * @param unitOfWork + * The function this test will execute. + */ def pure(unitOfWork: => Either[TestFailure, Unit]): Unit = apply(EitherT.fromEither[F](unitOfWork)) - /** - * Finalize and register this test with an effectful unit of work. - * - * @param unitOfWork The function this test will execute. - */ + /** Finalize and register this test with an effectful unit of work. + * + * @param unitOfWork + * The function this test will execute. + */ def effectful(unitOfWork: => F[Either[TestFailure, Unit]]): Unit = apply(EitherT(unitOfWork)) - /** - * Finalize and register this test with an effectful unit of work. - * - * @param unitOfWork The function this test will execute. - */ + /** Finalize and register this test with an effectful unit of work. + * + * @param unitOfWork + * The function this test will execute. + */ def apply(unitOfWork: => EitherT[F, TestFailure, Unit]): Unit = - registry.register(new TestDefinition[F]( - name = name, - permanentId = permanentId, - documentation = documentation, - tags = tags.distinct.toList, - markers = markers.distinct.toList, - iterations = iterations, - unitOfWork = unitOfWork - )) + registry.register( + new TestDefinition[F]( + name = name, + permanentId = permanentId, + documentation = documentation, + tags = tags.distinct.toList, + markers = markers.distinct.toList, + iterations = iterations, + unitOfWork = unitOfWork + ) + ) - /** - * Builder to assist with defining tests. This builder is for tests which - * accept input via some producing function. - * - * @param registry Registry instance internal to a [[TestGroup]] for recording completed definitions. - * @param name The name of the test. - * @param permanentId The [[PermanentId]] of the test. - * @param inputFunction The function that provides input to this test. - * @param tags 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. - */ - protected final class InputTestBuilder[F[_]: Async, Input]( + /** Builder to assist with defining tests. This builder is for tests which + * accept input via some producing function. + * + * @param registry + * Registry instance internal to a [[TestGroup]] for recording completed + * definitions. + * @param name + * The name of the test. + * @param permanentId + * The [[PermanentId]] of the test. + * @param inputFunction + * The function that provides input to this test. + * @param tags + * 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 name: TestDefinition.Name, val permanentId: PermanentId, @@ -293,91 +317,100 @@ object TestGroup: private val tags: ListBuffer[Tag], private val markers: ListBuffer[Marker], private var documentation: Option[String] = None, - private var iterations: TestIterations = TestIterations.One, + private var iterations: TestIterations = TestIterations.One ): - /** - * Supply documentation for this test. - * - * @param docs The documentation for this test. - * @return This builder. - */ + + /** Supply documentation for this test. + * + * @param docs + * The documentation for this test. + * @return + * This builder. + */ def document(docs: String): InputTestBuilder[F, Input] = documentation = Some(docs) this - /** - * Add additional [[Test.Tag]] to this test definition. - * - * @param additionalTags The list of new tags. - * @return This builder. - */ - def tagged(additionalTags: Tag*): InputTestBuilder[F, Input] = + /** Add additional [[Test.Tag]] to this test definition. + * + * @param additionalTags + * The list of new tags. + * @return + * This builder. + */ + def tagged(additionalTags: Tag*): InputTestBuilder[F, Input] = val _ = tags.addAll(additionalTags) this - /** - * Add the [[TestDefinition.Marker.Ignored]] marker to this test definition. - * - * @return This builder. - */ - def ignored(): InputTestBuilder[F, Input] = + /** Add the [[TestDefinition.Marker.Ignored]] marker to this test + * definition. + * + * @return + * This builder. + */ + def ignored(): InputTestBuilder[F, Input] = val _ = markers.addOne(Marker.Ignored) this - /** - * Add one or more [[TestDefinition.Marker]] to this test definition. - * - * @param additionalMarkers The list of markers to add. - * @return This builder. - */ - def marked(additionalMarkers: Marker*): InputTestBuilder[F, Input] = + /** Add one or more [[TestDefinition.Marker]] to this test definition. + * + * @param additionalMarkers + * The list of markers to add. + * @return + * This builder. + */ + def marked(additionalMarkers: Marker*): InputTestBuilder[F, Input] = val _ = markers.addAll(additionalMarkers) this - /** - * Set the number of times this test should iterate. - * - * @param iters The number of iterations. - * @return This builder. - */ + /** Set the number of times this test should iterate. + * + * @param iters + * The number of iterations. + * @return + * This builder. + */ def iterate(iters: TestIterations): InputTestBuilder[F, Input] = iterations = iters this - /** - * Finalize and register this test with a pure unit of work. - * - * @param unitOfWork The function this test will execute. - */ + /** Finalize and register this test with a pure unit of work. + * + * @param unitOfWork + * The function this test will execute. + */ def pure(unitOfWork: Input => Either[TestFailure, Unit]): Unit = apply(input => EitherT(Async[F].delay(unitOfWork(input)))) - /** - * Finalize and register this test with an effectful unit of work. - * - * @param unitOfWork The function this test will execute. - */ + /** Finalize and register this test with an effectful unit of work. + * + * @param unitOfWork + * The function this test will execute. + */ def effectful(unitOfWork: Input => F[Either[TestFailure, Unit]]): Unit = apply(input => EitherT(unitOfWork(input))) - /** - * Finalize and register this test with an effectful unit of work. - * - * @param unitOfWork The function this test will execute. - */ + /** Finalize and register this test with an effectful unit of work. + * + * @param unitOfWork + * The function this test will execute. + */ def apply(unitOfWork: Input => EitherT[F, TestFailure, Unit]): Unit = - registry.register(new TestDefinition[F]( - name = name, - permanentId = permanentId, - documentation = documentation, - tags = tags.distinct.toList, - markers = markers.distinct.toList, - iterations = iterations, - unitOfWork = EitherT.right(inputFunction).flatMap(unitOfWork) - )) + registry.register( + new TestDefinition[F]( + name = name, + permanentId = permanentId, + documentation = documentation, + tags = tags.distinct.toList, + markers = markers.distinct.toList, + iterations = iterations, + unitOfWork = EitherT.right(inputFunction).flatMap(unitOfWork) + ) + ) - protected final class Registry[F[_]]: - val mapping: ConcurrentHashMap[PermanentId, TestDefinition[F]] = + final protected class Registry[F[_]]: + + val mapping: ConcurrentHashMap[PermanentId, TestDefinition[F]] = new ConcurrentHashMap[PermanentId, TestDefinition[F]] def register(test: TestDefinition[F]): Unit = @@ -385,8 +418,7 @@ object TestGroup: throw new IllegalArgumentException( s"Attempted to register test with duplicate Permanent ID '${test.permanentId.show}'." ) - else - mapping.put(test.permanentId, test) + else mapping.put(test.permanentId, test) def toList(): List[TestDefinition[F]] = mapping.values().asScala.toList diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala index 9be1e4c..75c4a00 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestGroupDefinition.scala @@ -2,16 +2,20 @@ package gs.test.v0.definition 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. * - * @param name The group name. Not considered to be unique. - * @param documentation Arbitrary documentation for this group of tests. - * @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. + * @param name + * The group name. Not considered to be unique. + * @param documentation + * Arbitrary documentation for this group of tests. + * @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[_]]( val name: TestGroupDefinition.Name, @@ -27,20 +31,20 @@ final class TestGroupDefinition[F[_]]( 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 object Name: - /** - * Instantiate a new [[TestGroup.Name]]. This name is not unique, has no - * constraints, and only exists for display purposes. - * - * @param name The candidate string. - * @return The new [[TestGroup.Name]] instance. - */ + /** Instantiate a new [[TestGroup.Name]]. This name is not unique, has no + * constraints, and only exists for display purposes. + * + * @param name + * The candidate string. + * @return + * The new [[TestGroup.Name]] instance. + */ def apply(name: String): Name = name given CanEqual[Name, Name] = CanEqual.derived diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestIterations.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestIterations.scala index 4860812..01cff55 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestIterations.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestIterations.scala @@ -2,36 +2,34 @@ package gs.test.v0.definition import cats.Show -/** - * Opaque type that represents the number of iterations a test should run. - * This value must be at least `1` (the default). To ignore a test, use the - * [[Test.Marker.Ignored]] marker. - */ +/** Opaque type that represents the number of iterations a test should run. This + * value must be at least `1` (the default). To ignore a test, use the + * [[Test.Marker.Ignored]] marker. + */ opaque type TestIterations = Int object TestIterations: def One: TestIterations = 1 - /** - * Validate and instantiate a new [[TestIterations]] instance. - * - * @param candidate The candidate value. Must be 1 or greater. - * @return The new [[TestIterations]], or an error if an invalid input is given. - */ + /** Validate and instantiate a new [[TestIterations]] instance. + * + * @param candidate + * The candidate value. Must be 1 or greater. + * @return + * The new [[TestIterations]], or an error if an invalid input is given. + */ def apply(candidate: Int): TestIterations = - if candidate < 1 then + if candidate < 1 then throw new IllegalArgumentException( s"Tests must iterate at least once. Received candidate '$candidate'." ) - else - candidate + else candidate given CanEqual[TestIterations, TestIterations] = CanEqual.derived given Show[TestIterations] = iters => iters.toString() - extension (iters: TestIterations) - def toInt(): Int = iters + extension (iters: TestIterations) def toInt(): Int = iters end TestIterations diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestSuite.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestSuite.scala index 4b83496..861cd80 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/TestSuite.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/TestSuite.scala @@ -1,16 +1,18 @@ 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 * for some project would likely comprise of a single suite. * * Within each suite is a list of [[TestGroup]], arbitrary ways to organize * individual [[Test]] definitions. * - * @param name The name of this test suite. - * @param documentation Arbitrary documentation for this suite of tests. - * @param groups List of [[TestGroup]] owned by this suite. + * @param name + * The name of this test suite. + * @param documentation + * Arbitrary documentation for this suite of tests. + * @param groups + * List of [[TestGroup]] owned by this suite. */ case class TestSuite[F[_]]( name: String, diff --git a/modules/api-definition/src/main/scala/gs/test/v0/definition/syntax.scala b/modules/api-definition/src/main/scala/gs/test/v0/definition/syntax.scala index ba29e0f..3e8da9c 100644 --- a/modules/api-definition/src/main/scala/gs/test/v0/definition/syntax.scala +++ b/modules/api-definition/src/main/scala/gs/test/v0/definition/syntax.scala @@ -5,8 +5,7 @@ import cats.data.EitherT import cats.effect.Sync import cats.syntax.all.* -/** - * String interpolator for [[Tag]]. Shorthand for producing new [[Tag]] +/** String interpolator for [[Tag]]. Shorthand for producing new [[Tag]] * instances. * * {{{ @@ -14,11 +13,9 @@ import cats.syntax.all.* * val tag1: TestDefinition.Tag = tag"example" * }}} */ -extension (sc: StringContext) - def tag(args: Any*): Tag = Tag(sc.s(args*)) +extension (sc: StringContext) 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. * * {{{ @@ -29,35 +26,37 @@ extension (sc: StringContext) extension (sc: StringContext) 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? - * @return The failing test result. + * @param message + * 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)) -/** - * 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? - * @return The failing test result. + * @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). +/** Request this test to fail (lifted into EitherT). * - * @param message The message to report - why did this test fail? - * @return The failing test result. + * @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). +/** Shorthand for indicating a passing test (pure form). * * ## Example * @@ -70,12 +69,12 @@ def failT[F[_]: Applicative](message: String): EitherT[F, TestFailure, Unit] = * test(pid"ex", "Example Test").pure { pass() } * }}} * - * @return The passing test result. + * @return + * The passing test result. */ 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 * @@ -88,13 +87,13 @@ def pass(): Either[TestFailure, Unit] = Right(()) * 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]] = Applicative[F].pure(Right(())) -/** - * Shorthand for indicating a passing test (lifted into EitherT). +/** Shorthand for indicating a passing test (lifted into EitherT). * * ## Example * @@ -107,35 +106,45 @@ def passF[F[_]: Applicative](): F[Either[TestFailure, Unit]] = * test(pid"ex", "Example Test") { passT() } * }}} * - * @return The passing test result. + * @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 +/** 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. + * @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 + 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) - } + checks.foldLeft(initial) { + ( + acc, + result + ) => + acc.flatMap { + case Right(_) => result + case err => Sync[F].pure(err) + } } diff --git a/modules/api-definition/src/test/scala/gs/test/v0/GroupImplementationTests.scala b/modules/api-definition/src/test/scala/gs/test/v0/GroupImplementationTests.scala index f3746f1..5db9eda 100644 --- a/modules/api-definition/src/test/scala/gs/test/v0/GroupImplementationTests.scala +++ b/modules/api-definition/src/test/scala/gs/test/v0/GroupImplementationTests.scala @@ -1,15 +1,14 @@ package gs.test.v0.definition -import munit.* import cats.effect.IO - import gs.test.v0.definition.{Tag => GsTag} +import munit.* class GroupImplementationTests extends FunSuite: - import GroupImplementationTests.* + import GroupImplementationTests.* test("should support a group with a simple, pure, test") { - val g1 = new G1 + val g1 = new G1 val group = g1.compile() assertEquals(group.name, TestGroupDefinition.Name("G1")) assertEquals(group.documentation, None) @@ -33,7 +32,7 @@ class GroupImplementationTests extends FunSuite: } test("should support a group with all values set") { - val g2 = new G2 + val g2 = new G2 val group = g2.compile() assertEquals(group.name, TestGroupDefinition.Name("G2")) assertEquals(group.documentation, Some("docs")) @@ -57,7 +56,7 @@ class GroupImplementationTests extends FunSuite: } test("should support a simple group with a configured test") { - val g3 = new G3 + val g3 = new G3 val group = g3.compile() assertEquals(group.name, TestGroupDefinition.Name("G3")) assertEquals(group.documentation, None) @@ -92,28 +91,29 @@ object GroupImplementationTests: class G1 extends TestGroup.IO: override def name: String = "G1" - test(Ids.T1, "simple").pure { Right(()) } + test(Ids.T1, "simple").pure(Right(())) end G1 class G2 extends TestGroup.IO: - override def name: String = + + override def name: String = "G2" - override def documentation: Option[String] = + override def documentation: Option[String] = Some("docs") - override def tags: List[GsTag] = + override def tags: List[GsTag] = List(GsTag("tag")) - override def markers: List[Marker] = + override def markers: List[Marker] = List(Marker.Ignored) - beforeGroup { IO.unit } - afterGroup { IO.unit } - beforeEachTest { IO.unit } - afterEachTest { IO.unit } + beforeGroup(IO.unit) + afterGroup(IO.unit) + beforeEachTest(IO.unit) + afterEachTest(IO.unit) - test(Ids.T2, "inherit from group").pure { Right(()) } + test(Ids.T2, "inherit from group").pure(Right(())) end G2 class G3 extends TestGroup.IO: @@ -124,7 +124,8 @@ object GroupImplementationTests: .tagged(tag"tag1", tag"tag2") .marked(Marker.Ignored) .iterate(TestIterations(2)) - .pure { Right(()) } + .pure(Right(())) + end G3 end GroupImplementationTests