Now running real tests
This commit is contained in:
parent
3137fe4005
commit
5294ea669a
7 changed files with 141 additions and 85 deletions
|
@ -1,62 +0,0 @@
|
||||||
package gs.test.v0.api
|
|
||||||
|
|
||||||
import scala.reflect.*
|
|
||||||
|
|
||||||
sealed abstract class Assertion(val name: String)
|
|
||||||
|
|
||||||
object Assertion:
|
|
||||||
|
|
||||||
private def success(): Either[TestFailure, Unit] = Right(())
|
|
||||||
|
|
||||||
// TODO: Use a smart rendering solution, consider diffs.
|
|
||||||
// For now, this is fine -- add a diff library later as data.
|
|
||||||
def renderInput[A](value: A): String = value.toString()
|
|
||||||
|
|
||||||
case object IsEqualTo extends Assertion("isEqualTo"):
|
|
||||||
|
|
||||||
def evaluate[A: ClassTag](
|
|
||||||
candidate: A,
|
|
||||||
expected: A
|
|
||||||
)(
|
|
||||||
using
|
|
||||||
CanEqual[A, A]
|
|
||||||
)(
|
|
||||||
using
|
|
||||||
pos: SourcePosition
|
|
||||||
): 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)}'",
|
|
||||||
pos = pos
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
case object IsTrue extends Assertion("isTrue"):
|
|
||||||
|
|
||||||
def evaluate(
|
|
||||||
candidate: Boolean
|
|
||||||
)(
|
|
||||||
using
|
|
||||||
pos: SourcePosition
|
|
||||||
): Either[TestFailure, Unit] =
|
|
||||||
if candidate then success()
|
|
||||||
else
|
|
||||||
Left(
|
|
||||||
TestFailure.AssertionFailed(
|
|
||||||
assertionName = name,
|
|
||||||
inputs = Map("candidate" -> "Boolean"),
|
|
||||||
message = s"Expected '$candidate' to be 'true'.",
|
|
||||||
pos = pos
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
end Assertion
|
|
|
@ -1,9 +1,11 @@
|
||||||
package gs.test.v0.api
|
package gs.test.v0.api
|
||||||
|
|
||||||
|
import cats.data.EitherT
|
||||||
import cats.effect.Sync
|
import cats.effect.Sync
|
||||||
|
import scala.reflect.*
|
||||||
import scala.reflect.ClassTag
|
import scala.reflect.ClassTag
|
||||||
|
|
||||||
/** Opaque type used to check candidate values against expected values.
|
/** Opaque type used to check obtained values against expected values.
|
||||||
*/
|
*/
|
||||||
opaque type Check[A] = A
|
opaque type Check[A] = A
|
||||||
|
|
||||||
|
@ -11,18 +13,38 @@ object Check:
|
||||||
|
|
||||||
/** Instantiate a new Check.
|
/** Instantiate a new Check.
|
||||||
*
|
*
|
||||||
* @param candidate
|
* @param obtained
|
||||||
* The value to check.
|
* The value to check.
|
||||||
* @return
|
* @return
|
||||||
* The new [[Check]] instance.
|
* The new [[Check]] instance.
|
||||||
*/
|
*/
|
||||||
def apply[A](candidate: A): Check[A] = candidate
|
def apply[A](obtained: A): Check[A] = obtained
|
||||||
|
|
||||||
extension [A](candidate: Check[A])
|
private def testPassed(): TestResult = Right(())
|
||||||
|
|
||||||
|
private def render[A](value: A): String = value.toString()
|
||||||
|
|
||||||
|
private def assertionFailed(
|
||||||
|
assertionName: String,
|
||||||
|
inputs: Map[String, String],
|
||||||
|
message: String
|
||||||
|
)(
|
||||||
|
using
|
||||||
|
pos: SourcePosition
|
||||||
|
): TestResult = Left(
|
||||||
|
TestFailure.AssertionFailed(
|
||||||
|
assertionName = assertionName,
|
||||||
|
inputs = inputs,
|
||||||
|
message = message,
|
||||||
|
pos = pos
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
extension [A](obtained: Check[A])
|
||||||
/** @return
|
/** @return
|
||||||
* The unwrapped value of this [[Check]].
|
* The unwrapped value of this [[Check]].
|
||||||
*/
|
*/
|
||||||
def unwrap(): A = candidate
|
def unwrap(): A = obtained
|
||||||
|
|
||||||
/** ## Usage
|
/** ## Usage
|
||||||
*
|
*
|
||||||
|
@ -34,7 +56,7 @@ object Check:
|
||||||
* @param expected
|
* @param expected
|
||||||
* The expected value.
|
* The expected value.
|
||||||
* @return
|
* @return
|
||||||
* Successful test result if the candidate value is equal to the expected
|
* Successful test result if the obtained value is equal to the expected
|
||||||
* value, an error describing the test failure otherwise.
|
* value, an error describing the test failure otherwise.
|
||||||
*/
|
*/
|
||||||
def isEqualTo(
|
def isEqualTo(
|
||||||
|
@ -45,7 +67,18 @@ object Check:
|
||||||
ClassTag[A],
|
ClassTag[A],
|
||||||
SourcePosition
|
SourcePosition
|
||||||
): TestResult =
|
): TestResult =
|
||||||
Assertion.IsEqualTo.evaluate(candidate, expected)
|
if obtained == expected then testPassed()
|
||||||
|
else
|
||||||
|
val runtimeType = classTag[A].runtimeClass.getName()
|
||||||
|
assertionFailed(
|
||||||
|
assertionName = "isEqualTo",
|
||||||
|
inputs = Map(
|
||||||
|
"obtained" -> runtimeType,
|
||||||
|
"expected" -> runtimeType
|
||||||
|
),
|
||||||
|
message =
|
||||||
|
s"'${render(obtained)}' was not equal to '${render(expected)}'"
|
||||||
|
)
|
||||||
|
|
||||||
/** ## Usage
|
/** ## Usage
|
||||||
*
|
*
|
||||||
|
@ -60,8 +93,8 @@ object Check:
|
||||||
* The expected value.
|
* The expected value.
|
||||||
* @return
|
* @return
|
||||||
* Effect that when evaluated will produce: successful test result if the
|
* Effect that when evaluated will produce: successful test result if the
|
||||||
* candidate value is equal to the expected value, an error describing
|
* obtained value is equal to the expected value, an error describing the
|
||||||
* the test failure otherwise.
|
* test failure otherwise.
|
||||||
*/
|
*/
|
||||||
def isEqualToF[F[_]: Sync](
|
def isEqualToF[F[_]: Sync](
|
||||||
expected: A
|
expected: A
|
||||||
|
@ -73,7 +106,7 @@ object Check:
|
||||||
): F[TestResult] =
|
): F[TestResult] =
|
||||||
Sync[F].delay(isEqualTo(expected))
|
Sync[F].delay(isEqualTo(expected))
|
||||||
|
|
||||||
extension (check: Check[Boolean])
|
extension (obtained: Check[Boolean])
|
||||||
|
|
||||||
/** ## Usage
|
/** ## Usage
|
||||||
*
|
*
|
||||||
|
@ -83,7 +116,7 @@ object Check:
|
||||||
* }}}
|
* }}}
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* Successful test result if the candidate value is `true`, an error
|
* Successful test result if the obtained value is `true`, an error
|
||||||
* describing the test failure otherwise.
|
* describing the test failure otherwise.
|
||||||
*/
|
*/
|
||||||
def isTrue(
|
def isTrue(
|
||||||
|
@ -91,7 +124,13 @@ object Check:
|
||||||
using
|
using
|
||||||
SourcePosition
|
SourcePosition
|
||||||
): TestResult =
|
): TestResult =
|
||||||
Assertion.IsTrue.evaluate(check)
|
if obtained then testPassed()
|
||||||
|
else
|
||||||
|
assertionFailed(
|
||||||
|
assertionName = "isTrue",
|
||||||
|
inputs = Map("obtained" -> "Boolean"),
|
||||||
|
message = s"Expected '$obtained' to be 'true'."
|
||||||
|
)
|
||||||
|
|
||||||
/** ## Usage
|
/** ## Usage
|
||||||
*
|
*
|
||||||
|
@ -104,7 +143,7 @@ object Check:
|
||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
* Effect that when evaluated will produce: successful test result if the
|
* Effect that when evaluated will produce: successful test result if the
|
||||||
* candidate value is `true`, an error describing the test failure
|
* obtained value is `true`, an error describing the test failure
|
||||||
* otherwise.
|
* otherwise.
|
||||||
*/
|
*/
|
||||||
def isTrueF[F[_]: Sync](
|
def isTrueF[F[_]: Sync](
|
||||||
|
@ -114,4 +153,11 @@ object Check:
|
||||||
): F[TestResult] =
|
): F[TestResult] =
|
||||||
Sync[F].delay(isTrue())
|
Sync[F].delay(isTrue())
|
||||||
|
|
||||||
|
def isTrueT[F[_]: Sync](
|
||||||
|
)(
|
||||||
|
using
|
||||||
|
SourcePosition
|
||||||
|
): EitherT[F, TestFailure, Any] =
|
||||||
|
EitherT(Sync[F].delay(isTrue()))
|
||||||
|
|
||||||
end Check
|
end Check
|
||||||
|
|
|
@ -180,4 +180,4 @@ def checkAllF[F[_]: Sync](
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def check[A](candidate: A): Check[A] = Check(candidate)
|
def check[A](obtained: A): Check[A] = Check(obtained)
|
||||||
|
|
|
@ -132,12 +132,4 @@ object GroupImplementationTests:
|
||||||
|
|
||||||
end G3
|
end G3
|
||||||
|
|
||||||
class G4[F[_]: Async] extends TestGroup[F]:
|
|
||||||
override def name: String = "G4"
|
|
||||||
|
|
||||||
// TODO: Make test entrypoint and test Trace[F]
|
|
||||||
test(Ids.T4, "Effectful test").effectful {
|
|
||||||
???
|
|
||||||
}
|
|
||||||
|
|
||||||
end GroupImplementationTests
|
end GroupImplementationTests
|
||||||
|
|
|
@ -9,6 +9,19 @@ import gs.test.v0.api.GroupResult
|
||||||
import gs.test.v0.api.SuiteExecution
|
import gs.test.v0.api.SuiteExecution
|
||||||
import gs.test.v0.api.TestExecution
|
import gs.test.v0.api.TestExecution
|
||||||
|
|
||||||
|
/** Reporter that collects all results into memory.
|
||||||
|
*
|
||||||
|
* ### Lifecycle
|
||||||
|
*
|
||||||
|
* This reporter is intended to be used to get results _exactly once_, after
|
||||||
|
* the engine has completed its work. The function `terminateAndGetResults`
|
||||||
|
* will remove all results from the internal collection.
|
||||||
|
*
|
||||||
|
* @param suiteExecution
|
||||||
|
* Collector for the [[SuiteExecution]].
|
||||||
|
* @param groupResults
|
||||||
|
* Collector for grouped test results.
|
||||||
|
*/
|
||||||
final class InMemoryReporter[F[_]: Async] private (
|
final class InMemoryReporter[F[_]: Async] private (
|
||||||
suiteExecution: Ref[F, Option[SuiteExecution]],
|
suiteExecution: Ref[F, Option[SuiteExecution]],
|
||||||
groupResults: Queue[F, Option[(GroupResult, List[TestExecution])]]
|
groupResults: Queue[F, Option[(GroupResult, List[TestExecution])]]
|
||||||
|
@ -34,14 +47,28 @@ final class InMemoryReporter[F[_]: Async] private (
|
||||||
*/
|
*/
|
||||||
override def endReport(): F[Unit] = groupResults.offer(None)
|
override def endReport(): F[Unit] = groupResults.offer(None)
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The recorded [[SuiteExecution]]. This will be empty unless `reportSuite`
|
||||||
|
* was called.
|
||||||
|
*/
|
||||||
def getSuiteExecution(): F[Option[SuiteExecution]] = suiteExecution.get
|
def getSuiteExecution(): F[Option[SuiteExecution]] = suiteExecution.get
|
||||||
|
|
||||||
|
/** Immediatelly call `endReport` (to ensure terminal state) and collect all
|
||||||
|
* recorded results. This is a destructive call that will remove the results
|
||||||
|
* from this reporter.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The list of extracted results.
|
||||||
|
*/
|
||||||
def terminateAndGetResults(): F[List[(GroupResult, List[TestExecution])]] =
|
def terminateAndGetResults(): F[List[(GroupResult, List[TestExecution])]] =
|
||||||
endReport() *>
|
endReport() *>
|
||||||
fs2.Stream.fromQueueNoneTerminated(groupResults).compile.toList
|
fs2.Stream.fromQueueNoneTerminated(groupResults).compile.toList
|
||||||
|
|
||||||
object InMemoryReporter:
|
object InMemoryReporter:
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* Resource that provides a new [[InMemoryReporter]].
|
||||||
|
*/
|
||||||
def provision[F[_]: Async]: Resource[F, InMemoryReporter[F]] =
|
def provision[F[_]: Async]: Resource[F, InMemoryReporter[F]] =
|
||||||
Resource.make(
|
Resource.make(
|
||||||
for
|
for
|
||||||
|
|
|
@ -2,9 +2,10 @@ package gs.test.v0.runtime.engine
|
||||||
|
|
||||||
import cats.effect.IO
|
import cats.effect.IO
|
||||||
import cats.effect.Resource
|
import cats.effect.Resource
|
||||||
import gs.test.v0.api.TestGroupDefinition
|
import gs.test.v0.api.*
|
||||||
import gs.test.v0.reporting.InMemoryReporter
|
import gs.test.v0.reporting.InMemoryReporter
|
||||||
import gs.test.v0.runtime.engine.TestEngineTests.EngineObservation
|
import gs.test.v0.runtime.engine.TestEngineTests.EngineObservation
|
||||||
|
import gs.test.v0.runtime.engine.TestEngineTests.G1
|
||||||
import gs.timing.v0.MonotonicProvider.ManualTickProvider
|
import gs.timing.v0.MonotonicProvider.ManualTickProvider
|
||||||
import gs.timing.v0.Timing
|
import gs.timing.v0.Timing
|
||||||
import gs.uuid.v0.UUID
|
import gs.uuid.v0.UUID
|
||||||
|
@ -34,6 +35,50 @@ class TestEngineTests extends IOSuite:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iotest("should run an engine with a single passing test") {
|
||||||
|
newEngine().use { obs =>
|
||||||
|
val g1 = new G1
|
||||||
|
val group = g1.compile()
|
||||||
|
for
|
||||||
|
suiteExecution <- obs.engine.runSuite(
|
||||||
|
suite = Generators.testSuite(),
|
||||||
|
tests = fs2.Stream.apply(group)
|
||||||
|
)
|
||||||
|
rootSpan <- obs.entryPoint.getSpan(EngineConstants.Tracing.RootSpan)
|
||||||
|
groupSpan <- obs.entryPoint.getSpan(EngineConstants.Tracing.FullGroup)
|
||||||
|
beforeGroupSpan <- obs.entryPoint.getSpan(
|
||||||
|
EngineConstants.Tracing.BeforeGroup
|
||||||
|
)
|
||||||
|
afterGroupSpan <- obs.entryPoint.getSpan(
|
||||||
|
EngineConstants.Tracing.AfterGroup
|
||||||
|
)
|
||||||
|
inGroupSpan <- obs.entryPoint.getSpan(EngineConstants.Tracing.InGroup)
|
||||||
|
fullTestSpan <- obs.entryPoint.getSpan(EngineConstants.Tracing.FullTest)
|
||||||
|
beforeTestSpan <- obs.entryPoint.getSpan(
|
||||||
|
EngineConstants.Tracing.BeforeTest
|
||||||
|
)
|
||||||
|
afterTestSpan <- obs.entryPoint.getSpan(
|
||||||
|
EngineConstants.Tracing.AfterTest
|
||||||
|
)
|
||||||
|
testSpan <- obs.entryPoint.getSpan(EngineConstants.Tracing.TestSpan)
|
||||||
|
results <- obs.reporter.terminateAndGetResults()
|
||||||
|
yield
|
||||||
|
assertEquals(rootSpan.isDefined, true)
|
||||||
|
assertEquals(groupSpan.isDefined, true)
|
||||||
|
assertEquals(beforeGroupSpan.isDefined, true)
|
||||||
|
assertEquals(afterGroupSpan.isDefined, true)
|
||||||
|
assertEquals(inGroupSpan.isDefined, true)
|
||||||
|
assertEquals(fullTestSpan.isDefined, true)
|
||||||
|
assertEquals(beforeTestSpan.isDefined, true)
|
||||||
|
assertEquals(afterTestSpan.isDefined, true)
|
||||||
|
assertEquals(testSpan.isDefined, true)
|
||||||
|
assertEquals(results.size, 1)
|
||||||
|
assertEquals(suiteExecution.seen, 1L)
|
||||||
|
assertEquals(suiteExecution.passed, 1L)
|
||||||
|
assertEquals(suiteExecution.failed, 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def emptyStream[A]: fs2.Stream[IO, A] =
|
private def emptyStream[A]: fs2.Stream[IO, A] =
|
||||||
fs2.Stream.empty
|
fs2.Stream.empty
|
||||||
|
|
||||||
|
@ -78,4 +123,11 @@ object TestEngineTests:
|
||||||
engine: TestEngine[IO]
|
engine: TestEngine[IO]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class G1 extends TestGroup[IO]:
|
||||||
|
override def name: String = "single-passing-test"
|
||||||
|
|
||||||
|
test(pid"engine:g1", "show that true is true") {
|
||||||
|
check(true).isTrueT()
|
||||||
|
}
|
||||||
|
|
||||||
end TestEngineTests
|
end TestEngineTests
|
||||||
|
|
|
@ -8,6 +8,7 @@ import natchez.Kernel
|
||||||
import natchez.Span
|
import natchez.Span
|
||||||
import natchez.Span.Options
|
import natchez.Span.Options
|
||||||
|
|
||||||
|
// TODO: This doesn't account for multiple spans with the same name.
|
||||||
final class TestEntryPoint private (
|
final class TestEntryPoint private (
|
||||||
spans: MapRef[IO, String, Option[TestSpan]]
|
spans: MapRef[IO, String, Option[TestSpan]]
|
||||||
) extends EntryPoint[IO]:
|
) extends EntryPoint[IO]:
|
||||||
|
|
Loading…
Add table
Reference in a new issue