diff --git a/build.sbt b/build.sbt index 805cddc..32a8ec6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -val scala3: String = "3.4.2" +val scala3: String = "3.5.0" ThisBuild / scalaVersion := scala3 ThisBuild / versionScheme := Some("semver-spec") @@ -9,6 +9,10 @@ ThisBuild / externalResolvers := Seq( "Garrity Software Releases" at "https://maven.garrity.co/gs" ) +ThisBuild / licenses := Seq( + "MIT" -> url("https://garrity.co/MIT.html") +) + val noPublishSettings = Seq( publish := {} ) diff --git a/modules/core/src/main/scala/gs/test/v0/GsTestError.scala b/modules/core/src/main/scala/gs/test/v0/GsTestError.scala deleted file mode 100644 index d30e048..0000000 --- a/modules/core/src/main/scala/gs/test/v0/GsTestError.scala +++ /dev/null @@ -1,15 +0,0 @@ -package gs.test.v0 - -sealed trait GsTestError - -object GsTestError: - - sealed trait TestDefinitionError extends GsTestError - - object TestDefinitionError: - - case class InvalidIterations(candidate: Int) extends TestDefinitionError - - end TestDefinitionError - -end GsTestError diff --git a/modules/core/src/main/scala/gs/test/v0/Marker.scala b/modules/core/src/main/scala/gs/test/v0/Marker.scala new file mode 100644 index 0000000..a28ad67 --- /dev/null +++ b/modules/core/src/main/scala/gs/test/v0/Marker.scala @@ -0,0 +1,22 @@ +package gs.test.v0 + +/** + * 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. + */ + case object Ignored extends Marker("ignored") + +end Marker diff --git a/modules/core/src/main/scala/gs/test/v0/Tag.scala b/modules/core/src/main/scala/gs/test/v0/Tag.scala new file mode 100644 index 0000000..8e18c5e --- /dev/null +++ b/modules/core/src/main/scala/gs/test/v0/Tag.scala @@ -0,0 +1,24 @@ +package gs.test.v0 + +import cats.Show + +/** + * 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. + */ + def apply(tag: String): Tag = tag + + given CanEqual[Tag, Tag] = CanEqual.derived + + given Show[Tag] = tag => tag + +end Tag diff --git a/modules/core/src/main/scala/gs/test/v0/TestDefinition.scala b/modules/core/src/main/scala/gs/test/v0/TestDefinition.scala index 3a1b4fc..9c2c54c 100644 --- a/modules/core/src/main/scala/gs/test/v0/TestDefinition.scala +++ b/modules/core/src/main/scala/gs/test/v0/TestDefinition.scala @@ -2,24 +2,25 @@ package gs.test.v0 import cats.data.EitherT import cats.Show -import gs.test.v0.GsTestError.TestDefinitionError.InvalidIterations /** * 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 tags The set of [[Test.Tag]] applicable to 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 f The effect that the test evaluates. + * @param unitOfWork The function that the test evaluates. */ final class TestDefinition[F[_]]( val name: TestDefinition.Name, val permanentId: PermanentId, val documentation: Option[String], - val tags: List[TestDefinition.Tag], - val markers: List[TestDefinition.Marker], - val iterations: TestDefinition.Iterations, + val tags: List[Tag], + val markers: List[Marker], + val iterations: TestIterations, val unitOfWork: EitherT[F, TestFailure, Unit] ) @@ -47,78 +48,4 @@ object TestDefinition: end Name - /** - * Opaque type representing tags that may be assigned to a [[Test]]. - */ - opaque type Tag = String - - object Tag: - - /** - * Instantiate a new [[Test.Tag]]. - * - * @param tag The candidate string. - * @return The new [[Test.Tag]] instance. - */ - def apply(tag: String): Tag = tag - - given CanEqual[Tag, Tag] = CanEqual.derived - - given Show[Tag] = tag => tag - - end Tag - - /** - * 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 [[Test.Marker]] is present on a test, the test will be ignored. - */ - case object Ignored extends Marker("ignored") - - end 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 Iterations = Int - - object Iterations: - - def One: Iterations = 1 - - /** - * Validate and instantiate a new [[Iterations]] instance. - * - * @param candidate The candidate value. Must be 1 or greater. - * @return The new [[Iterations]], or an error if an invalid input is given. - */ - def validate(candidate: Int): Either[GsTestError, Iterations] = - if candidate < 1 then - Left(InvalidIterations(candidate)) - else - Right(candidate) - - given CanEqual[Iterations, Iterations] = CanEqual.derived - - given Show[Iterations] = iters => iters.toString() - - extension (iters: Iterations) - def toInt(): Int = iters - - end Iterations - end TestDefinition diff --git a/modules/core/src/main/scala/gs/test/v0/TestGroup.scala b/modules/core/src/main/scala/gs/test/v0/TestGroup.scala index bdbd6d0..a8f53cc 100644 --- a/modules/core/src/main/scala/gs/test/v0/TestGroup.scala +++ b/modules/core/src/main/scala/gs/test/v0/TestGroup.scala @@ -3,9 +3,6 @@ package gs.test.v0 import cats.syntax.all.* import cats.effect.Async import scala.collection.mutable.ListBuffer -import gs.test.v0.TestDefinition.Tag -import gs.test.v0.TestDefinition.Marker -import gs.test.v0.TestDefinition.Iterations import cats.data.EitherT import java.util.concurrent.ConcurrentHashMap import scala.jdk.CollectionConverters.* @@ -117,7 +114,7 @@ object TestGroup: private val tags: ListBuffer[Tag], private val markers: ListBuffer[Marker], private var documentation: Option[String] = None, - private var iterations: TestDefinition.Iterations = Iterations.One, + private var iterations: TestIterations = TestIterations.One, ): /** * Supply documentation for this test. @@ -164,7 +161,7 @@ object TestGroup: * @param iters The number of iterations. * @return This builder. */ - def iterate(iters: Iterations): TestBuilder[F] = + def iterate(iters: TestIterations): TestBuilder[F] = iterations = iters this @@ -239,7 +236,7 @@ object TestGroup: private val tags: ListBuffer[Tag], private val markers: ListBuffer[Marker], private var documentation: Option[String] = None, - private var iterations: TestDefinition.Iterations = Iterations.One, + private var iterations: TestIterations = TestIterations.One, ): /** * Supply documentation for this test. @@ -286,7 +283,7 @@ object TestGroup: * @param iters The number of iterations. * @return This builder. */ - def iterate(iters: Iterations): InputTestBuilder[F, Input] = + def iterate(iters: TestIterations): InputTestBuilder[F, Input] = iterations = iters this diff --git a/modules/core/src/main/scala/gs/test/v0/TestGroupDefinition.scala b/modules/core/src/main/scala/gs/test/v0/TestGroupDefinition.scala index 45bb3f1..7ee6cf6 100644 --- a/modules/core/src/main/scala/gs/test/v0/TestGroupDefinition.scala +++ b/modules/core/src/main/scala/gs/test/v0/TestGroupDefinition.scala @@ -9,14 +9,15 @@ import cats.Show * * @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 [[Test]] within the 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[_]]( val name: TestGroupDefinition.Name, val documentation: Option[String], - val testTags: List[TestDefinition.Tag], - val testMarkers: List[TestDefinition.Marker], + val testTags: List[Tag], + val testMarkers: List[Marker], val beforeGroup: Option[F[Unit]], val afterGroup: Option[F[Unit]], val beforeEachTest: Option[F[Unit]], @@ -27,7 +28,7 @@ final class TestGroupDefinition[F[_]]( object TestGroupDefinition: /** - * Opaque type representing names that may be assigned to [[TestGroup]]. + * Opaque type representing names that may be assigned to test groups. */ opaque type Name = String diff --git a/modules/core/src/main/scala/gs/test/v0/TestIterations.scala b/modules/core/src/main/scala/gs/test/v0/TestIterations.scala new file mode 100644 index 0000000..6bea812 --- /dev/null +++ b/modules/core/src/main/scala/gs/test/v0/TestIterations.scala @@ -0,0 +1,37 @@ +package gs.test.v0 + +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 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. + */ + def apply(candidate: Int): TestIterations = + if candidate < 1 then + throw new IllegalArgumentException( + s"Tests must iterate at least once. Received candidate '$candidate'." + ) + else + candidate + + given CanEqual[TestIterations, TestIterations] = CanEqual.derived + + given Show[TestIterations] = iters => iters.toString() + + extension (iters: TestIterations) + def toInt(): Int = iters + +end TestIterations diff --git a/modules/core/src/main/scala/gs/test/v0/syntax.scala b/modules/core/src/main/scala/gs/test/v0/syntax.scala new file mode 100644 index 0000000..742a5ab --- /dev/null +++ b/modules/core/src/main/scala/gs/test/v0/syntax.scala @@ -0,0 +1,25 @@ +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*)) diff --git a/modules/core/src/test/scala/gs/test/v0/GroupImplementationTests.scala b/modules/core/src/test/scala/gs/test/v0/GroupImplementationTests.scala index a6109d8..8d0b754 100644 --- a/modules/core/src/test/scala/gs/test/v0/GroupImplementationTests.scala +++ b/modules/core/src/test/scala/gs/test/v0/GroupImplementationTests.scala @@ -1,6 +1,9 @@ package gs.test.v0 import munit.* +import cats.effect.IO + +import gs.test.v0.{Tag => GsTag} class GroupImplementationTests extends FunSuite: import GroupImplementationTests.* @@ -25,7 +28,55 @@ class GroupImplementationTests extends FunSuite: assertEquals(t.documentation, None) assertEquals(t.tags, List.empty) assertEquals(t.markers, List.empty) - assertEquals(t.iterations, TestDefinition.Iterations.One) + assertEquals(t.iterations, TestIterations.One) + case _ => fail("Unexpected number of defined tests.") + } + + test("should support a group with all values set") { + val g2 = new G2 + val group = g2.toGroupDefinition() + assertEquals(group.name, TestGroupDefinition.Name("G2")) + assertEquals(group.documentation, Some("docs")) + assertEquals(group.testTags, List(GsTag("tag"))) + assertEquals(group.testMarkers, List(Marker.Ignored)) + assertEquals(group.beforeGroup.isDefined, true) + assertEquals(group.afterGroup.isDefined, true) + assertEquals(group.beforeEachTest.isDefined, true) + assertEquals(group.afterEachTest.isDefined, true) + assertEquals(group.tests.size, 1) + + group.tests match + case t :: Nil => + assertEquals(t.name, TestDefinition.Name("inherit from group")) + assertEquals(t.permanentId, Ids.T2) + assertEquals(t.documentation, None) + assertEquals(t.tags, List(GsTag("tag"))) + assertEquals(t.markers, List(Marker.Ignored)) + assertEquals(t.iterations, TestIterations.One) + case _ => fail("Unexpected number of defined tests.") + } + + test("should support a simple group with a configured test") { + val g3 = new G3 + val group = g3.toGroupDefinition() + assertEquals(group.name, TestGroupDefinition.Name("G3")) + assertEquals(group.documentation, None) + assertEquals(group.testTags, List.empty) + assertEquals(group.testMarkers, List.empty) + assertEquals(group.beforeGroup, None) + assertEquals(group.afterGroup, None) + assertEquals(group.beforeEachTest, None) + assertEquals(group.afterEachTest, None) + assertEquals(group.tests.size, 1) + + group.tests match + case t :: Nil => + assertEquals(t.name, TestDefinition.Name("configure test")) + assertEquals(t.permanentId, Ids.T3) + assertEquals(t.documentation, Some("docs")) + assertEquals(t.tags, List(tag"tag1", tag"tag2")) + assertEquals(t.markers, List(Marker.Ignored)) + assertEquals(t.iterations, TestIterations(2)) case _ => fail("Unexpected number of defined tests.") } @@ -33,7 +84,9 @@ object GroupImplementationTests: object Ids: - val T1: PermanentId = PermanentId("t1") + val T1: PermanentId = pid"t1" + val T2: PermanentId = pid"t2" + val T3: PermanentId = pid"t3" end Ids @@ -42,4 +95,36 @@ object GroupImplementationTests: test(Ids.T1, "simple").pure { Right(()) } end G1 + class G2 extends TestGroup.IO: + override def name: String = + "G2" + + override def documentation: Option[String] = + Some("docs") + + override def tags: List[GsTag] = + List(GsTag("tag")) + + override def markers: List[Marker] = + List(Marker.Ignored) + + beforeGroup { IO.unit } + afterGroup { IO.unit } + beforeEachTest { IO.unit } + afterEachTest { IO.unit } + + test(Ids.T2, "inherit from group").pure { Right(()) } + end G2 + + class G3 extends TestGroup.IO: + override def name: String = "G3" + + test(Ids.T3, "configure test") + .document("docs") + .tagged(tag"tag1", tag"tag2") + .marked(Marker.Ignored) + .iterate(TestIterations(2)) + .pure { Right(()) } + end G3 + end GroupImplementationTests diff --git a/project/plugins.sbt b/project/plugins.sbt index c708d3a..eb382dc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -29,5 +29,5 @@ externalResolvers := Seq( ) addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.1.0") -addSbtPlugin("gs" % "sbt-garrity-software" % "0.3.0") +addSbtPlugin("gs" % "sbt-garrity-software" % "0.4.0") addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0")