├── project
├── build.properties
├── plugins.sbt
└── util.scala
├── version.sbt
├── http4s
└── src
│ ├── main
│ └── scala
│ │ └── klk
│ │ ├── Http4sTestBase.scala
│ │ └── Http4s.scala
│ └── test
│ ├── resources
│ └── logback-test.xml
│ └── scala
│ └── klk
│ └── Http4sSuiteTest.scala
├── sbt
└── src
│ ├── test
│ └── scala
│ │ └── klk
│ │ ├── BasicTest.scala
│ │ └── DepTest.scala
│ └── main
│ └── scala
│ └── klk
│ ├── SimpleTest.scala
│ ├── KlkFramework.scala
│ ├── SbtResources.scala
│ └── KlkTask.scala
├── core
└── src
│ ├── main
│ └── scala
│ │ ├── klk
│ │ ├── package.scala
│ │ ├── TestFramework.scala
│ │ ├── TestInterface.scala
│ │ ├── SimpleTest.scala
│ │ ├── StringColor.scala
│ │ ├── ComposeTest.scala
│ │ ├── MeasureTest.scala
│ │ ├── TransformThunk.scala
│ │ ├── StripResources.scala
│ │ ├── Concurrency.scala
│ │ ├── KlkTest.scala
│ │ ├── ExecuteThunk.scala
│ │ ├── SharedResource.scala
│ │ ├── DslTest.scala
│ │ ├── TestBuilder.scala
│ │ ├── EvalSuite.scala
│ │ ├── Laws.scala
│ │ ├── KlkResult.scala
│ │ ├── Suite.scala
│ │ ├── Classes.scala
│ │ └── Property.scala
│ │ └── org
│ │ └── scalacheck
│ │ └── ForAll.scala
│ └── test
│ └── scala
│ └── klk
│ ├── Fs2Test.scala
│ ├── EitherTTest.scala
│ ├── ExceptionTest.scala
│ ├── ResourceTest.scala
│ ├── PropNoShrinkTest.scala
│ ├── PropShrinkTest.scala
│ ├── ForAllTest.scala
│ ├── LawsTest.scala
│ ├── SharedResourceTest.scala
│ ├── SuiteSequentialTest.scala
│ ├── SuiteUnlessTest.scala
│ ├── SuiteParallelTest.scala
│ ├── DepTest.scala
│ └── KlkSpecification.scala
├── http4s-sbt
└── src
│ ├── main
│ └── scala
│ │ └── klk
│ │ └── Http4sTest.scala
│ └── test
│ ├── resources
│ └── logback-test.xml
│ └── scala
│ └── klk
│ └── Http4sSbtTest.scala
├── LICENCE
└── README.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.3.2
2 |
--------------------------------------------------------------------------------
/version.sbt:
--------------------------------------------------------------------------------
1 | version in ThisBuild := "0.5.3-SNAPSHOT"
2 |
--------------------------------------------------------------------------------
/http4s/src/main/scala/klk/Http4sTestBase.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | trait Http4sTestBase[RunF[_], FR]
4 | extends ComposeTest[RunF, FR]
5 | with Http4s[RunF, FR]
6 |
--------------------------------------------------------------------------------
/sbt/src/test/scala/klk/BasicTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.IO
4 |
5 | class BasicTest
6 | extends IOTest
7 | {
8 | test("basic sbt test")(IO.pure(1 == 1))
9 | }
10 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/package.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.instances.AllInstances
4 | import cats.syntax.AllSyntax
5 |
6 | object `package`
7 | extends AllSyntax
8 | with AllInstances
9 |
--------------------------------------------------------------------------------
/http4s-sbt/src/main/scala/klk/Http4sTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.IO
4 |
5 | trait Http4sTest[F[_]]
6 | extends Http4sTestBase[F, SbtResources]
7 |
8 | trait Http4sIOTest
9 | extends Http4sTest[IO]
10 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.0")
2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3")
3 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.10")
4 | addSbtPlugin("org.lyranthe.sbt" % "partial-unification" % "1.1.2")
5 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/Fs2Test.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.IO
4 | import fs2.Stream
5 |
6 | class Fs2Test
7 | extends KlkSpecification[IO]
8 | {
9 | "compile a Stream" >>
10 | assert("stream")(_.apply(Stream[IO, Boolean](true)))(KlkResult.bool(true))
11 | }
12 |
--------------------------------------------------------------------------------
/sbt/src/main/scala/klk/SimpleTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.IO
4 |
5 | trait SimpleTest[F[_]]
6 | extends SimpleTestBase[F, SbtResources]
7 |
8 | trait IOTest
9 | extends SimpleTest[IO]
10 |
11 | trait SimpleComposeTest[F[_]]
12 | extends ComposeTest[F, SbtResources]
13 |
14 | trait IOComposeTest
15 | extends SimpleComposeTest[IO]
16 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/EitherTTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.data.EitherT
4 | import cats.effect.IO
5 |
6 | class EitherTTest
7 | extends KlkSpecification[IO]
8 | {
9 | val target: KlkResult[Unit] =
10 | KlkResult.Single((), true, KlkResult.Details.NoDetails)
11 |
12 | assert("EitherT")(_(EitherT.right[Unit](IO.pure(1 == 1))))(target)
13 | }
14 |
--------------------------------------------------------------------------------
/http4s/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/http4s-sbt/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/ExceptionTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.IO
4 |
5 | class ExceptionTest
6 | extends KlkSpecification[IO]
7 | {
8 | object E
9 | extends Throwable
10 |
11 | def frame1: Boolean =
12 | throw E
13 |
14 | val target: KlkResult[Unit] =
15 | KlkResult.Fatal(E)
16 |
17 | assert("exception")(_.apply(IO(frame1)))(target)
18 | }
19 |
--------------------------------------------------------------------------------
/http4s-sbt/src/test/scala/klk/Http4sSbtTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.IO
4 | import org.http4s.{HttpApp, Request, Response}
5 |
6 | class Http4sSbtTest
7 | extends Http4sIOTest
8 | {
9 | def tests: Suite[IO, Unit, Unit] =
10 | server
11 | .app(HttpApp.liftF(IO.pure(Response[IO]())))
12 | .test { builder =>
13 | builder.test("http4s") { client =>
14 | client.fetch(Request[IO]())(_ => IO.pure(true))
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/TestFramework.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Applicative
4 |
5 | trait TestFramework[RunF[_], Resources]
6 | {
7 | def reporter(res: Resources): TestReporter[RunF]
8 | }
9 |
10 | object TestFramework
11 | {
12 | implicit def TestFramework_NoopResources[RunF[_]: Applicative]: TestFramework[RunF, NoopResources.type] =
13 | new TestFramework[RunF, NoopResources.type] {
14 | def reporter(res: NoopResources.type): TestReporter[RunF] =
15 | NoopTestReporter()
16 | }
17 | }
18 |
19 | object NoopResources
20 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/ResourceTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.{IO, Resource}
4 |
5 | class ResourceTest
6 | extends KlkSpecification[IO]
7 | {
8 | // TODO 2.12 compat
9 | val res1: Resource[IO, Int] = Resource.pure[IO, Int](1)
10 |
11 | // TODO 2.12 compat
12 | val res2: Resource[IO, Int] = Resource.pure[IO, Int](1)
13 |
14 | val target: KlkResult[Unit] =
15 | KlkResult.success(KlkResult.Details.NoDetails)
16 |
17 | assert("resource")(_.resource(res1).resource(res2)((i: Int) => (j: Int) => IO.pure(i == j)))(target)
18 | }
19 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/TestInterface.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Monad
4 |
5 | private[klk] trait TestMarker
6 |
7 | trait FrameworkTest[FR]
8 | extends TestMarker
9 | {
10 | def run(frameworkResources: FR): List[TestStats]
11 |
12 | }
13 |
14 | abstract class TestBase[RunF[_]: Monad: Compute: MeasureTest: TestFramework[*[_], FR], FR]
15 | extends FrameworkTest[FR]
16 | {
17 | def tests: Suite[RunF, Unit, Unit]
18 |
19 | def run(frameworkResources: FR): List[TestStats] =
20 | Compute[RunF].run(EvalSuite(tests).run(RunTestResources.cons(frameworkResources)))
21 | }
22 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/PropNoShrinkTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.data.NonEmptyList
4 | import cats.effect.IO
5 |
6 | class PropNoShrinkTest
7 | extends KlkSpecification[IO]
8 | {
9 | val target: KlkResult[Unit] =
10 | KlkResult.Single((), true, KlkResult.Details.NoDetails)
11 |
12 | "property test, no shrink" >> {
13 | val result = test(_.forallNoShrink((l: List[Int]) => IO(l.size < 5)))
14 | result match {
15 | case KlkResult.Single((), success, KlkResult.Details.Simple(NonEmptyList(head, _))) =>
16 | success.must(beFalse).and(head.must(startWith("failed after")))
17 | case _ => false.must(beTrue)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/PropShrinkTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.data.NonEmptyList
4 | import cats.effect.IO
5 |
6 | // TODO specify seed and test the value
7 | class PropShrinkTest
8 | extends KlkSpecification[IO]
9 | {
10 | val target: KlkResult[Unit] =
11 | KlkResult.Single((), true, KlkResult.Details.NoDetails)
12 |
13 | "property test, shrink" >> {
14 | val result = test(_.forall((i: Int) => IO.pure(i > 0)))
15 | result match {
16 | case KlkResult.Single((), success, KlkResult.Details.Simple(NonEmptyList(head, _))) =>
17 | success.must(beFalse).and(head.must(startWith("failed after")))
18 | case a => a.must_==("wrong result")
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/SimpleTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Show
4 | import cats.data.NonEmptyList
5 | import cats.kernel.Eq
6 |
7 | trait SimpleAssertions
8 | {
9 | def assert(desc: String)(value: Boolean): KlkResult[Unit] =
10 | KlkResult(value)(KlkResult.Details.Simple(NonEmptyList.one(desc)))
11 |
12 | def assertEqual[A: Show](target: A)(candidate: A)(implicit eql: Eq[A]): KlkResult[A] =
13 | KlkResult.Single(
14 | candidate,
15 | eql.eqv(target, candidate),
16 | KlkResult.Details.Complex(NonEmptyList.one("values are not equal"), target.show, candidate.show),
17 | )
18 | }
19 |
20 | trait SimpleTestBase[F[_], FR]
21 | extends DslTest[F, FR]
22 | with SimpleAssertions
23 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/ForAllTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.data.Kleisli
4 | import cats.effect.IO
5 | import org.scalacheck.ForAllNoShrink
6 | import org.scalacheck.Gen.Parameters
7 | import org.scalacheck.Test.{Parameters => TestParameters}
8 | import org.specs2.mutable.Specification
9 |
10 | class ForAllTest
11 | extends Specification
12 | {
13 | "forall" >> {
14 | val f: PropertyTest[IO] = ForAllNoShrink { (a: Int) =>
15 | ForAllNoShrink { (b: Int) =>
16 | PropertyTest(Kleisli.pure(PropResult.bool(a != b)))
17 | }
18 | }
19 | val params = ScalacheckParams.cons(TestParameters.default, Parameters.default.withInitialSeed(10L))
20 | val result = PropertyTest.run(ConsConcurrent.io)(params)(f).unsafeRunSync()
21 | result.success.must(beFalse)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/StringColor.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | trait StringColor
4 | {
5 | def color(s: String, col: String): String
6 | }
7 |
8 | object StringColors
9 | {
10 | implicit val noColor =
11 | new StringColor {
12 | def color(s: String, col: String) = s
13 | }
14 |
15 | implicit val color =
16 | new StringColor {
17 | import Console.RESET
18 |
19 | def color(s: String, col: String) = col + s + RESET
20 | }
21 | }
22 |
23 | object StringColor
24 | {
25 | implicit class StringColorOps(s: String)(implicit sc: StringColor)
26 | {
27 | import Console._
28 | def red = sc.color(s, RED)
29 | def green = sc.color(s, GREEN)
30 | def yellow = sc.color(s, YELLOW)
31 | def blue = sc.color(s, BLUE)
32 | def magenta = sc.color(s, MAGENTA)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/ComposeTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.{Id, MonadError}
4 | import cats.effect.{Bracket, Resource}
5 | import shapeless.HNil
6 |
7 | abstract class ComposeTest[RunF[_]: MonadError[*[_], Throwable]: Compute: MeasureTest: TestFramework[*[_], FR], FR]
8 | extends TestBase[RunF, FR]
9 | {
10 | private[this] case class Cons(desc: String)
11 | extends TestAdder[RunF, Id, Suite[RunF, Unit, *]]
12 | {
13 | def apply[A](thunk: Id[RunF[KlkResult[A]]]): Suite[RunF, Unit, A] =
14 | Suite.single(KlkTest.plain(desc)(thunk))
15 | }
16 |
17 | def test(desc: String): TestBuilder[RunF, HNil, Id, Suite[RunF, Unit, *]] =
18 | TestBuilder(TestResources.empty)(Cons(desc))
19 |
20 | def sharedResource[R]
21 | (resource: Resource[RunF, R])
22 | (tests: SharedResource[RunF, R, FR] => Suite[RunF, R, Unit])
23 | (implicit bracket: Bracket[RunF, Throwable])
24 | : Suite[RunF, Unit, Unit] =
25 | SharedResource.suite(resource)(tests)
26 | }
27 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/LawsTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Functor
4 | import cats.effect.IO
5 | import cats.kernel.Eq
6 | import cats.laws.discipline.FunctorTests
7 | import org.scalacheck.ScalacheckShapeless._
8 | import org.specs2.matcher.MatchResult
9 |
10 | case class Funky[A](a: A)
11 |
12 | object Funky
13 | {
14 | implicit def Functor_Funky: Functor[Funky] =
15 | new Functor[Funky] {
16 | def map[A, B](fa: Funky[A])(f: A => B): Funky[B] =
17 | Funky(f(fa.a))
18 | }
19 |
20 | implicit def Eq_Funky[A: Eq]: Eq[Funky[A]] =
21 | Eq.fromUniversalEquals
22 | }
23 |
24 | class FunctorLawsTest
25 | extends KlkSpecification[IO]
26 | {
27 | val target: KlkResult[Unit] =
28 | KlkResult.success(KlkResult.Details.NoDetails)
29 |
30 | val check: KlkResult[Unit] => MatchResult[Any] =
31 | a => KlkResult.successful(a).must_==(true)
32 |
33 | assertWith("laws")(_.laws(IO.pure(FunctorTests[Funky].functor[Int, Int, Int])))(check)
34 | }
35 |
--------------------------------------------------------------------------------
/http4s/src/test/scala/klk/Http4sSuiteTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.concurrent.ExecutionContext
4 |
5 | import cats.data.OptionT
6 | import cats.effect.{ContextShift, IO, Timer}
7 | import org.http4s.{HttpRoutes, Request, Response}
8 |
9 | class Http4sSuiteTest
10 | extends KlkSpecification[IO]
11 | {
12 | implicit def cs: ContextShift[IO] =
13 | IO.contextShift(ExecutionContext.global)
14 |
15 | implicit def timer: Timer[IO] =
16 | IO.timer(ExecutionContext.global)
17 |
18 | def routes: HttpRoutes[IO] =
19 | HttpRoutes.liftF(OptionT.pure(Response[IO]()))
20 |
21 | val test: Suite[IO, Unit, Unit] =
22 | Http4s.server[IO, NoopResources.type].routes(routes).test { res =>
23 | res.test("http4s") { client =>
24 | client.fetch(Request[IO]())(_ => IO.pure(true))
25 | }
26 | }
27 |
28 | "http4s test" >> {
29 | EvalSuite(test)
30 | .run(RunTestResources.cons(NoopResources))
31 | .map(_.map { case TestStats(desc, _, success, _, _) => (desc, success) })
32 | .map(_.must_==(List(("http4s", true))))
33 | .unsafeRunSync()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/MeasureTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | import cats.Eval
6 | import cats.effect.{Clock, Sync}
7 |
8 | trait MeasureTest[F[_]]
9 | {
10 | def apply[A](thunk: F[A]): F[(A, Long)]
11 | }
12 |
13 | object MeasureTest
14 | {
15 | implicit def MeasureTest_Sync[F[_]: Sync]: MeasureTest[F] =
16 | new MeasureTest[F] {
17 | val clock: Clock[F] = Clock.create[F]
18 |
19 | def now: F[Long] =
20 | clock.realTime(TimeUnit.MILLISECONDS)
21 |
22 | def apply[A](thunk: F[A]): F[(A, Long)] =
23 | for {
24 | startTime <- now
25 | result <- thunk
26 | endTime <- now
27 | } yield (result, endTime - startTime)
28 | }
29 |
30 | implicit def Measure_Eval: MeasureTest[Eval] =
31 | new MeasureTest[Eval] {
32 | def now: Eval[Long] =
33 | Eval.always(System.currentTimeMillis())
34 |
35 | def apply[A](thunk: Eval[A]): Eval[(A, Long)] =
36 | for {
37 | startTime <- now
38 | result <- thunk
39 | endTime <- now
40 | } yield (result, endTime - startTime)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/SharedResourceTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.data.NonEmptyList
4 | import cats.effect.{IO, Resource}
5 | import shapeless.HNil
6 |
7 | class SharedResTest
8 | extends KlkSharedResourceSpecification[IO, Int]
9 | {
10 | def resource: Resource[IO, Int] =
11 | // TODO 2.12 compat
12 | Resource.pure[IO, Int](86)
13 |
14 | val testResource: Resource[IO, Int] =
15 | // TODO 2.12 compat
16 | Resource.pure[IO, Int](4)
17 |
18 | def srTest(builder: TestBuilder[IO, HNil, Function1[Int, ?], λ[a => Int => IO[KlkResult[a]]]])
19 | : List[Int => IO[KlkResult[Unit]]] =
20 | List(
21 | builder(i => IO.pure(i == 86)),
22 | builder(i => IO.pure(i == 68)),
23 | builder.resource(testResource)((i: Int) => (j: Int) => IO.pure(i + j < 90))
24 | )
25 |
26 | val target: KlkResult[Unit] =
27 | KlkResult.Multi(
28 | NonEmptyList.of(
29 | KlkResult.success(KlkResult.Details.NoDetails),
30 | KlkResult.failure(KlkResult.Details.NoDetails),
31 | KlkResult.failure(KlkResult.Details.NoDetails),
32 | )
33 | )
34 |
35 | assert("shared resource")(srTest)(target)
36 | }
37 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/SuiteSequentialTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Eval
4 | import cats.data.NonEmptyList
5 | import org.specs2.matcher.MatchResult
6 | import org.specs2.mutable.Specification
7 |
8 | class SuiteSequentialTest
9 | extends Specification
10 | {
11 | def stats(name: String, success: Boolean): TestStats =
12 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one(name))), success, false, 5L)
13 |
14 | def one(name: String)(continue: Boolean): Suite[Eval, Unit, String] =
15 | Suite.Suspend(_ => _ =>
16 | Eval.now(Suite.Output(Suite.Output.Details.Value(name), continue, List(stats(name, continue))))
17 | )
18 |
19 | def tests: Suite[Eval, Unit, String] =
20 | Suite.sequential(one("1")(false), one("2")(true), one("3")(true)) >>
21 | one("4")(true)
22 |
23 | def target: List[TestStats] =
24 | List(
25 | stats("1", false),
26 | stats("2", true),
27 | stats("3", true),
28 | )
29 |
30 | def test: Eval[MatchResult[Any]] =
31 | EvalSuite(tests).run(RunTestResources.cons[Eval](NoopResources))
32 | .map(_ must_== target)
33 |
34 | "sequential" >> test.value
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/TransformThunk.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.effect.Bracket
4 | import shapeless.HList
5 |
6 | trait TransformTestThunk[RunF[_], ResParams <: HList, Params, Thunk]
7 | {
8 | type Value
9 |
10 | def apply(resources: TestResources[ResParams])(thunk: Thunk): RunF[KlkResult[Value]]
11 | }
12 |
13 | object TransformTestThunk
14 | {
15 | type Aux[RunF[_], ResParams <: HList, Params, Thunk, V] =
16 | TransformTestThunk[RunF, ResParams, Params, Thunk] { type Value = V }
17 |
18 | implicit def TransformTestThunk_Any
19 | [RunF[_]: Bracket[*[_], Throwable], ResParams <: HList, Params, Thunk, Thunk0, TestF[_], Output, V]
20 | (
21 | implicit
22 | strip: StripResources.Aux[RunF, ResParams, Thunk, Thunk0],
23 | execute: ExecuteThunk.Aux[Params, Thunk0, TestF, Output],
24 | compile: Compile.Aux[TestF, RunF, Output, V],
25 | )
26 | : TransformTestThunk.Aux[RunF, ResParams, Params, Thunk, V] =
27 | new TransformTestThunk[RunF, ResParams, Params, Thunk] {
28 | type Value = V
29 | def apply(resources: TestResources[ResParams])(thunk: Thunk): RunF[KlkResult[Value]] =
30 | strip(resources)(thunk).use(t => compile(execute(t)))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/SuiteUnlessTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Eval
4 | import cats.data.NonEmptyList
5 | import org.specs2.matcher.MatchResult
6 | import org.specs2.mutable.Specification
7 |
8 | class SuiteUnlessTest
9 | extends Specification
10 | {
11 | def stats(name: String, success: Boolean): TestStats =
12 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one(name))), success, false, 5L)
13 |
14 | def one(name: String)(continue: Boolean): Suite[Eval, Unit, String] =
15 | Suite.Suspend(_ => _ =>
16 | Eval.now(Suite.Output(Suite.Output.Details.Value(name), continue, List(stats(name, continue))))
17 | )
18 |
19 | def tests: Suite[Eval, Unit, String] =
20 | one("0")(true) >>
21 | (one("1")(false) <+> (one("2")(true) <+> one("3")(true))) >>
22 | one("4")(true)
23 |
24 | def target: List[TestStats] =
25 | List(
26 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one("0"))), true, false, 5L),
27 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one("1"))), false, true, 5L),
28 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one("2"))), true, false, 5L),
29 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one("4"))), true, false, 5L),
30 | )
31 |
32 | def test: Eval[MatchResult[Any]] =
33 | EvalSuite(tests).run(RunTestResources.cons[Eval](NoopResources))
34 | .map(_ must_== target)
35 |
36 | "unless" >> test.value
37 | }
38 |
--------------------------------------------------------------------------------
/sbt/src/test/scala/klk/DepTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.concurrent.ExecutionContext
4 |
5 | import cats.effect.{ContextShift, IO, Resource}
6 |
7 | class DepTest
8 | extends ComposeTest[IO, SbtResources]
9 | {
10 | def testSuccess: IO[KlkResult[Int]] =
11 | IO.pure(KlkResult.Single(555, true, KlkResult.Details.NoDetails))
12 |
13 | def testFail: IO[KlkResult[Int]] =
14 | IO.raiseError(new Exception("boom"))
15 |
16 | implicit def cs: ContextShift[IO] =
17 | IO.contextShift(ExecutionContext.global)
18 |
19 | // TODO SR builder should be generic.
20 | // instead of calling builder.test, call test().
21 | // the constructed data should be parameterized in the thunk, to be evaluated by the SR.
22 | // composition should work so that only thunks having the resource parameter compile.
23 | def tests: Suite[IO, Unit, Unit] =
24 | for {
25 | _ <- sharedResource(Resource.pure[IO, Int](5))(
26 | builder =>
27 | builder.test("five is 4")(five => IO.pure(five == 4)) <+>
28 | builder.test("five is 5")(five => IO.pure(five == 5))
29 | )
30 | a <- test("test 1")(testSuccess)
31 | _ <- test("test 555")(IO.pure(a == 555))
32 | _ <- test("test 2")(testFail) <+> test("test 3")(testSuccess)
33 | _ <- {
34 | Suite.parallel(test("test 4a")(testSuccess), test("test 4b")(testSuccess)).void <+>
35 | test("test 5")(testFail).void
36 | }
37 | _ <- test("test 7")(testSuccess)
38 | } yield ()
39 | }
40 |
--------------------------------------------------------------------------------
/sbt/src/main/scala/klk/KlkFramework.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import sbt.testing.{Fingerprint, Framework, Runner, SubclassFingerprint, Task, TaskDef}
4 |
5 | object KlkClassFingerprint
6 | extends SubclassFingerprint
7 | {
8 | def superclassName: String =
9 | "klk.TestMarker"
10 |
11 | def isModule: Boolean =
12 | false
13 |
14 | def requireNoArgConstructor: Boolean =
15 | true
16 | }
17 |
18 | object KlkModuleFingerprint
19 | extends SubclassFingerprint
20 | {
21 | def superclassName: String =
22 | "klk.TestMarker"
23 |
24 | def isModule: Boolean =
25 | true
26 |
27 | def requireNoArgConstructor: Boolean =
28 | true
29 | }
30 |
31 | object KlkConfigFingerprint
32 | extends SubclassFingerprint
33 | {
34 | def superclassName: String =
35 | "klk.TestConfig"
36 |
37 | def isModule: Boolean =
38 | true
39 |
40 | def requireNoArgConstructor: Boolean =
41 | true
42 | }
43 |
44 | class KlkFramework
45 | extends Framework
46 | {
47 | def name: String =
48 | "kallikrein"
49 |
50 | def fingerprints: Array[Fingerprint] =
51 | Array(KlkClassFingerprint, KlkModuleFingerprint, KlkConfigFingerprint)
52 |
53 | def runner(args0: Array[String], remoteArgs0: Array[String], testClassLoader: ClassLoader): Runner =
54 | new Runner {
55 | def args: Array[String] =
56 | args0
57 |
58 | def done: String =
59 | ""
60 |
61 | def remoteArgs: Array[String] =
62 | remoteArgs0
63 |
64 | def tasks(taskDefs: Array[TaskDef]): Array[Task] =
65 | KlkTasks.process(testClassLoader)(taskDefs)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/StripResources.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Applicative
4 | import cats.effect.{Bracket, Resource}
5 | import shapeless.{::, HList, HNil}
6 |
7 | trait StripResources[F[_], ResParams <: HList, ThunkF]
8 | {
9 | type Thunk
10 |
11 | def apply(resources: TestResources[ResParams])(thunk: ThunkF): Resource[F, Thunk]
12 | }
13 |
14 | object StripResources
15 | {
16 | type Aux[F[_], ResParams <: HList, ThunkF, Thunk0] =
17 | StripResources[F, ResParams, ThunkF] {
18 | type Thunk = Thunk0
19 | }
20 |
21 | implicit def StripResources_HNil[TestF[_], RunF[_]: Applicative, Output]
22 | : StripResources.Aux[RunF, HNil, TestF[Output], TestF[Output]] =
23 | new StripResources[RunF, HNil, TestF[Output]] {
24 | type Thunk = TestF[Output]
25 | def apply(resources: TestResources[HNil])(thunk: Thunk): Resource[RunF, Thunk] =
26 | // TODO 2.12 compat
27 | Resource.pure[RunF, Thunk](thunk)
28 | }
29 |
30 | implicit def StripResources_HList[TestF[_], RunF[_]: Bracket[*[_], Throwable], H, T <: HList, ThunkF, Output]
31 | (implicit next: StripResources.Aux[RunF, T, ThunkF, TestF[Output]])
32 | : Aux[RunF, Resource[RunF, H] :: T, H => ThunkF, TestF[Output]] =
33 | new StripResources[RunF, Resource[RunF, H] :: T, H => ThunkF] {
34 | type Thunk = TestF[Output]
35 | def apply(resources: TestResources[Resource[RunF, H] :: T])(thunk: H => ThunkF): Resource[RunF, TestF[Output]] =
36 | for {
37 | h <- resources.resources.head
38 | t <- next(TestResources(resources.resources.tail))(thunk(h))
39 | } yield t
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/Concurrency.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import java.util.concurrent.{ExecutorService, Executors}
4 |
5 | import scala.concurrent.ExecutionContext
6 |
7 | import cats.effect.{ContextShift, IO, Resource, Sync}
8 | import fs2.Stream
9 |
10 | object Concurrency
11 | {
12 | val defaultNum: Int =
13 | Runtime.getRuntime.availableProcessors
14 |
15 | def fixedPoolWith[F[_]: Sync](num: Int): F[ExecutorService] =
16 | Sync[F].delay(Executors.newFixedThreadPool(num))
17 |
18 | def ec[F[_]: Sync](pool: F[ExecutorService]): Resource[F, ExecutionContext] =
19 | Resource.make(pool)(es => Sync[F].delay(es.shutdown()))
20 | .map(ExecutionContext.fromExecutorService)
21 |
22 | def fixedPool[F[_]: Sync]: F[ExecutorService] =
23 | fixedPoolWith(defaultNum)
24 |
25 | def fixedPoolEc[F[_]: Sync]: Resource[F, ExecutionContext] =
26 | ec(fixedPool[F])
27 |
28 | def fixedPoolEcWith(num: Int): Resource[IO, ExecutionContext] =
29 | ec(fixedPoolWith[IO](num))
30 |
31 | def fixedPoolEcStream: Stream[IO, ExecutionContext] =
32 | Stream.resource(fixedPoolEc[IO])
33 |
34 | def cs(pool: IO[ExecutorService]): Resource[IO, ContextShift[IO]] =
35 | ec(pool)
36 | .map(IO.contextShift(_))
37 |
38 | def fixedPoolCsWith(num: Int): Resource[IO, ContextShift[IO]] =
39 | cs(fixedPoolWith[IO](num))
40 |
41 | def fixedPoolCs: Resource[IO, ContextShift[IO]] =
42 | cs(fixedPool[IO])
43 |
44 | def fixedPoolCsStreamWith(num: Int): Stream[IO, ContextShift[IO]] =
45 | Stream.resource(fixedPoolCsWith(num))
46 |
47 | def fixedPoolCsStream: Stream[IO, ContextShift[IO]] =
48 | Stream.resource(fixedPoolCs)
49 | }
50 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/KlkTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.util.control.NonFatal
4 |
5 | import cats.{Functor, MonadError}
6 | import cats.effect.{Bracket, Resource}
7 |
8 | case class KlkTest[F[_], Res, A](desc: String, thunk: TestReporter[F] => Res => F[KlkResult[A]])
9 |
10 | object KlkTest
11 | {
12 | def runPlain[F[_]: Functor, FR, A]
13 | (test: KlkTest[F, Unit, A])
14 | (fwRes: FR)
15 | (implicit compute: Compute[F], fw: TestFramework[F, FR])
16 | : KlkResult[A] =
17 | compute.run(test.thunk(fw.reporter(fwRes))(()))
18 |
19 | def runResource[F[_]: Bracket[*[_], Throwable], Res, FR, A]
20 | (resource: Resource[F, Res])
21 | (tests: collection.Seq[KlkTest[F, Res, A]])
22 | (fwRes: FR)
23 | (implicit compute: Compute[F], fw: TestFramework[F, FR])
24 | : KlkResult[A] =
25 | compute.run(resource.use(r => tests.toList.traverse(_.thunk(fw.reporter(fwRes))(r))).map(_.combineAll))
26 |
27 | def executeThunk[RunF[_]: MonadError[*[_], Throwable], SharedRes, FR, A]
28 | (desc: String)
29 | (thunk: SharedRes => RunF[KlkResult[A]])
30 | (reporter: TestReporter[RunF])
31 | (sharedRes: SharedRes)
32 | : RunF[KlkResult[A]] =
33 | for {
34 | testResult <- thunk(sharedRes)
35 | .recover { case NonFatal(e) => KlkResult.Fatal(e) }
36 | _ <- TestReporter.report[RunF, A](reporter)(desc)(testResult)
37 | } yield testResult
38 |
39 | def cons[RunF[_]: MonadError[*[_], Throwable], Res, A]
40 | (desc: String)
41 | (thunk: Res => RunF[KlkResult[A]])
42 | : KlkTest[RunF, Res, A] =
43 | KlkTest(desc, KlkTest.executeThunk(desc)(thunk))
44 |
45 | def plain[RunF[_]: MonadError[*[_], Throwable], A](desc: String)(thunk: RunF[KlkResult[A]]): KlkTest[RunF, Unit, A] =
46 | cons(desc)(_ => thunk)
47 | }
48 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | # Blue Oak Model License
2 |
3 | Version 1.0.0
4 |
5 | ## Purpose
6 |
7 | This license gives everyone as much permission to work with
8 | this software as possible, while protecting contributors
9 | from liability.
10 |
11 | ## Acceptance
12 |
13 | In order to receive this license, you must agree to its
14 | rules. The rules of this license are both obligations
15 | under that agreement and conditions to your license.
16 | You must not do anything with this software that triggers
17 | a rule that you cannot or will not follow.
18 |
19 | ## Copyright
20 |
21 | Each contributor licenses you to do everything with this
22 | software that would otherwise infringe that contributor's
23 | copyright in it.
24 |
25 | ## Notices
26 |
27 | You must ensure that everyone who gets a copy of
28 | any part of this software from you, with or without
29 | changes, also gets the text of this license or a link to
30 | .
31 |
32 | ## Excuse
33 |
34 | If anyone notifies you in writing that you have not
35 | complied with [Notices](#notices), you can keep your
36 | license by taking all practical steps to comply within 30
37 | days after the notice. If you do not do so, your license
38 | ends immediately.
39 |
40 | ## Patent
41 |
42 | Each contributor licenses you to do everything with this
43 | software that would otherwise infringe any patent claims
44 | they can license or become able to license.
45 |
46 | ## Reliability
47 |
48 | No contributor can revoke this license.
49 |
50 | ## No Liability
51 |
52 | ***As far as the law allows, this software comes as is,
53 | without any warranty or condition, and no contributor
54 | will be liable to anyone for any damages related to this
55 | software or this license, under any kind of legal claim.***
56 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/SuiteParallelTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.data.NonEmptyList
4 | import cats.effect.{ContextShift, IO}
5 | import cats.effect.concurrent.Ref
6 | import org.specs2.matcher.MatchResult
7 | import org.specs2.mutable.Specification
8 |
9 | class SuiteParallelTest
10 | extends Specification
11 | {
12 | def stats(name: String, success: Boolean): TestStats =
13 | TestStats("test", List(KlkResult.Details.Simple(NonEmptyList.one(name))), success, false, 5L)
14 |
15 | def wait
16 | (counter: Ref[IO, Int])
17 | (target: Int)
18 | : IO[Unit] =
19 | counter.get.flatMap { i => if (i < target) wait(counter)(target) else IO.unit }
20 |
21 | def one
22 | (counter: Ref[IO, Int])
23 | (target: Int)
24 | (continue: Boolean)
25 | : Suite[IO, Unit, Int] =
26 | Suite.Suspend(
27 | _ => _ =>
28 | wait(counter)(target) *>
29 | counter.update(_ + 1) *>
30 | IO.pure(Suite.Output(Suite.Output.Details.Value(target), continue, List(stats(target.toString, continue))))
31 | )
32 |
33 | def tests(counter: Ref[IO, Int])(implicit cs: ContextShift[IO]): Suite[IO, Unit, Int] =
34 | Suite.parallel(one(counter)(3)(false), one(counter)(2)(true), one(counter)(1)(true)) >>
35 | one(counter)(4)(true)
36 |
37 | def target: List[TestStats] =
38 | List(
39 | stats("3", false),
40 | stats("2", true),
41 | stats("1", true),
42 | )
43 |
44 | def test: IO[MatchResult[Any]] =
45 | Concurrency.fixedPoolCs.use(
46 | implicit cs =>
47 | for {
48 | counter <- Ref.of[IO, Int](1)
49 | result <- EvalSuite(tests(counter)).run(RunTestResources.cons[IO](NoopResources))
50 | } yield result must_== target
51 | )
52 |
53 | "parallel" >> test.unsafeRunSync()
54 | }
55 |
--------------------------------------------------------------------------------
/sbt/src/main/scala/klk/SbtResources.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.{Functor, Traverse}
4 | import cats.data.NonEmptyList
5 | import cats.effect.Sync
6 | import sbt.testing.Logger
7 |
8 | case class SbtTestLog(loggers: Array[Logger])
9 |
10 | object SbtTestLog
11 | {
12 | def sync[F[_]: Sync, T[_]: Traverse](log: SbtTestLog)(f: Logger => String => Unit): T[String] => F[Unit] =
13 | lines =>
14 | log.loggers.toList.traverse_(logger => lines.traverse_(line => Sync[F].delay(f(logger)(line))))
15 |
16 | def unsafe[T[_]: Functor](log: SbtTestLog)(f: Logger => String => Unit)(lines: T[String]): Unit =
17 | log.loggers.toList.foreach(logger => lines.map(line => f(logger)(line)))
18 | }
19 |
20 | case class SbtResources(log: SbtTestLog)
21 |
22 | object SbtResources
23 | extends SbtResourcesInstances
24 |
25 | trait SbtResourcesInstances
26 | {
27 | implicit def TestFramework_SbtResources[RunF[_]: Sync]: TestFramework[RunF, SbtResources] =
28 | new TestFramework[RunF, SbtResources] {
29 | def reporter(res: SbtResources): TestReporter[RunF] =
30 | SbtTestReporter(res.log)
31 | }
32 | }
33 |
34 | case class SbtTestReporter[F[_]: Sync](log: SbtTestLog)
35 | extends TestReporter[F]
36 | {
37 | def result: String => Boolean => F[Unit] =
38 | desc => success =>
39 | SbtTestLog.sync[F, NonEmptyList](log)(if (success) _.info else _.error)
40 | .apply(TestReporter.formatResult(desc)(success))
41 |
42 | def failure: KlkResult.Details => F[Unit] =
43 | SbtTestLog.sync[F, NonEmptyList](log)(_.error).compose(Indent[NonEmptyList](2)).compose(TestReporter.formatFailure)
44 |
45 | def fatal: Throwable => F[Unit] =
46 | SbtTestLog.sync[F, NonEmptyList](log)(_.error).compose(Indent[NonEmptyList](2)).compose(TestReporter.formatFatal)
47 | }
48 |
--------------------------------------------------------------------------------
/project/util.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | import Keys._
4 |
5 | object Util
6 | extends AutoPlugin
7 | {
8 | object autoImport
9 | {
10 | val github = "https://github.com/tek"
11 | val projectName = "kallikrein"
12 | val repo = s"$github/$projectName"
13 |
14 | def noPublish: List[Setting[_]] = List(skip in publish := true)
15 |
16 | def basicProject(p: Project) =
17 | p.settings(
18 | organization := "io.tryp",
19 | fork := true,
20 | scalacOptions ++= List(
21 | "-deprecation",
22 | "-unchecked",
23 | "-feature",
24 | "-language:higherKinds",
25 | "-language:existentials",
26 | "-Ywarn-value-discard",
27 | "-Ywarn-unused:imports",
28 | "-Ywarn-unused:implicits",
29 | "-Ywarn-unused:params",
30 | "-Ywarn-unused:patvars",
31 | )
32 | )
33 |
34 | def pro(n: String) =
35 | basicProject(Project(n, file(n)))
36 | .settings(
37 | name := s"$projectName-$n",
38 | addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"),
39 | addCompilerPlugin("org.typelevel" % "kind-projector" % "0.11.0" cross CrossVersion.full),
40 | publishMavenStyle := true,
41 | publishTo := Some(
42 | if (isSnapshot.value) Opts.resolver.sonatypeSnapshots
43 | else Resolver.url("sonatype staging", url("https://oss.sonatype.org/service/local/staging/deploy/maven2"))
44 | ),
45 | licenses := List("BOML" -> url("https://blueoakcouncil.org/license/1.0.0")),
46 | homepage := Some(url(repo)),
47 | scmInfo := Some(ScmInfo(url(repo), s"scm:git@github.com:tek/$projectName")),
48 | developers := List(Developer(id="tek", name="Torsten Schmits", email="torstenschmits@gmail.com",
49 | url=url(github))),
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/ExecuteThunk.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Monad
4 |
5 | import org.scalacheck.Prop
6 | import org.typelevel.discipline.Laws
7 |
8 | final class NoExecutionParams
9 |
10 | trait ExecuteThunk[Params, Thunk]
11 | {
12 | type TestF[A]
13 | type Output
14 |
15 | def apply(thunk: Thunk): TestF[Output]
16 | }
17 |
18 | trait ExecuteThunk1
19 | {
20 | type Aux[Params, Thunk, TestF0[_], Output0] =
21 | ExecuteThunk[Params, Thunk] {
22 | type TestF[A] = TestF0[A]
23 | type Output = Output0
24 | }
25 |
26 | implicit def ExecuteThunk_Any[TestF0[_], Output0]
27 | : ExecuteThunk.Aux[NoExecutionParams, TestF0[Output0], TestF0, Output0] =
28 | new ExecuteThunk[NoExecutionParams, TestF0[Output0]] {
29 | type TestF[A] = TestF0[A]
30 | type Output = Output0
31 | def apply(thunk: TestF0[Output]): TestF[Output] =
32 | thunk
33 | }
34 | }
35 |
36 | object ExecuteThunk
37 | extends ExecuteThunk1
38 | {
39 | implicit def ExecuteThunk_PropertyTestOutput[Trans, Thunk, TestF0[_]]
40 | (implicit propRun: PropRun.Aux[Thunk, Trans, TestF0])
41 | : ExecuteThunk.Aux[Trans, Thunk, TestF0, PropertyTestResult] =
42 | new ExecuteThunk[Trans, Thunk] {
43 | type TestF[A] = TestF0[A]
44 | type Output = PropertyTestResult
45 | def apply(thunk: Thunk): TestF[PropertyTestResult] =
46 | PropRun(propRun)(thunk)
47 | }
48 |
49 | implicit def ExecuteThunk_LawsResult[TestF0[_]: Monad, L <: Laws]
50 | (implicit propRun: PropRun.Aux[TestF0[Prop], LawsParams, TestF0])
51 | : ExecuteThunk.Aux[LawsParams, TestF0[L#RuleSet], TestF0, LawsResult] =
52 | new ExecuteThunk[LawsParams, TestF0[L#RuleSet]] {
53 | type TestF[A] = TestF0[A]
54 | type Output = LawsResult
55 | def apply(thunk: TestF[L#RuleSet]): TestF[LawsResult] =
56 | thunk.flatMap(LawsTest(propRun))
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/SharedResource.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.collection.mutable
4 |
5 | import cats.MonadError
6 | import cats.data.Const
7 | import cats.effect.{Bracket, Resource}
8 | import shapeless.HNil
9 |
10 | case class SharedResource[RunF[_]: Compute: MonadError[*[_], Throwable]: TestFramework[*[_], FR], SharedRes, FR]
11 | (resource: Resource[RunF, SharedRes])
12 | {
13 | private[this] case class Cons(desc: String)
14 | extends TestAdder[RunF, SharedRes => *, Suite[RunF, SharedRes, *]]
15 | {
16 | def apply[A](thunk: SharedRes => RunF[KlkResult[A]]): Suite[RunF, SharedRes, A] =
17 | Suite.single(KlkTest.cons(desc)(thunk))
18 | }
19 |
20 | def test(desc: String): TestBuilder[RunF, HNil, SharedRes => *, Suite[RunF, SharedRes, *]] =
21 | TestBuilder(TestResources.empty)(Cons(desc))
22 | }
23 |
24 | object SharedResource
25 | {
26 | def suite[RunF[_]: Compute: TestFramework[*[_], FR]: Bracket[*[_], Throwable], SharedRes, FR, A]
27 | (resource: Resource[RunF, SharedRes])
28 | (tests: SharedResource[RunF, SharedRes, FR] => Suite[RunF, SharedRes, A])
29 | : Suite[RunF, Unit, A] =
30 | Suite.resource(resource, tests(SharedResource(resource)))
31 | }
32 |
33 | case class DslSharedResource[RunF[_]: MonadError[*[_], Throwable], SharedRes]
34 | (tests: mutable.Buffer[Suite[RunF, SharedRes, Unit]])
35 | {
36 | private[this] case class Add(desc: String)
37 | extends TestAdder[RunF, SharedRes => *, Const[Unit, *]]
38 | {
39 | def apply[A](thunk: SharedRes => RunF[KlkResult[A]]): Const[Unit, A] = {
40 | tests += Suite.single(KlkTest.cons(desc)(thunk)).void
41 | Const(())
42 | }
43 | }
44 |
45 | def test(desc: String): TestBuilder[RunF, HNil, Function1[SharedRes, *], Const[Unit, *]] =
46 | TestBuilder(TestResources.empty)(Add(desc))
47 | }
48 |
49 | object DslSharedResource
50 | {
51 | def cons[RunF[_]: MonadError[*[_], Throwable], SharedRes]: DslSharedResource[RunF, SharedRes] =
52 | DslSharedResource(mutable.Buffer.empty)
53 | }
54 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/DepTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.concurrent.ExecutionContext
4 |
5 | import cats.Id
6 | import cats.effect.{ContextShift, IO}
7 | import org.specs2.mutable.Specification
8 | import shapeless.HNil
9 |
10 | class DepTest
11 | extends Specification
12 | {
13 | case class Cons()
14 | extends TestAdder[IO, Id, λ[a => IO[KlkResult[a]]]]
15 | {
16 | def apply[A](thunk: Id[IO[KlkResult[A]]]): IO[KlkResult[A]] =
17 | thunk
18 | }
19 |
20 | def test: TestBuilder[IO, HNil, Id, λ[a => IO[KlkResult[a]]]] =
21 | // TODO 2.12 compat
22 | TestBuilder[IO, HNil, Id, λ[a => IO[KlkResult[a]]]](TestResources.empty)(Cons())
23 |
24 | def testSuccess: IO[KlkResult[Unit]] =
25 | test(IO.pure(true))
26 |
27 | def testFail: IO[KlkResult[Unit]] =
28 | test(IO.pure(false))
29 |
30 | def testThunk(desc: String)(thunk: IO[KlkResult[Unit]]): KlkTest[IO, Unit, Unit] =
31 | KlkTest.plain(desc)(thunk)
32 |
33 | def cons(desc: String)(thunk: IO[KlkResult[Unit]]): Suite[IO, Unit, Unit] =
34 | Suite.single(testThunk(desc)(thunk))
35 |
36 | def tests: Suite[IO, Unit, Unit] =
37 | for {
38 | _ <- cons("test 1")(testSuccess)
39 | _ <- cons("test 2")(testFail) <+> cons("test 3")(testSuccess)
40 | _ <- cons("test 4")(testSuccess) <+> cons("test 5")(testFail)
41 | _ <- cons("test 6")(testFail)
42 | _ <- cons("test 7")(testSuccess)
43 | } yield ()
44 |
45 | implicit def cs: ContextShift[IO] =
46 | IO.contextShift(ExecutionContext.global)
47 |
48 | val target: List[(String, Boolean)] =
49 | List(
50 | ("test 1", true),
51 | ("test 2", false),
52 | ("test 3", true),
53 | ("test 4", true),
54 | ("test 6", false),
55 | )
56 |
57 | "dependent tests" >> {
58 | EvalSuite(tests)
59 | .run(RunTestResources.cons(NoopResources))
60 | .map(_.map { case TestStats(desc, _, success, _, _) => (desc, success) })
61 | .map(_.must_==(target))
62 | .unsafeRunSync()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/DslTest.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.collection.mutable
4 |
5 | import cats.{Id, Monad}
6 | import cats.data.Const
7 | import cats.effect.{Bracket, Resource}
8 | import shapeless.HNil
9 |
10 | case class DslTests[RunF[_]: Monad: Compute: TestFramework[*[_], FR], FR]
11 | (tests: mutable.Buffer[Suite[RunF, Unit, Unit]])
12 | {
13 | def add[A](test: Suite[RunF, Unit, A]): Unit =
14 | tests += test.void
15 |
16 | def plain[A](test: KlkTest[RunF, Unit, A]): Unit =
17 | add(Suite.single(test).void)
18 |
19 | def resource[SharedRes]
20 | (resource: Resource[RunF, SharedRes], builder: DslSharedResource[RunF, SharedRes])
21 | (implicit bracket: Bracket[RunF, Throwable])
22 | : Unit =
23 | builder.tests.toList match {
24 | case head :: tail =>
25 | add(Suite.resource(resource, Suite.sequential(head, tail: _*)))
26 | case Nil =>
27 | add(Suite.Pure(()))
28 | }
29 | }
30 |
31 | object DslTests
32 | {
33 | def cons[RunF[_]: Monad: Compute: TestFramework[*[_], FR], FR]: DslTests[RunF, FR] =
34 | DslTests(mutable.Buffer.empty)
35 | }
36 |
37 | abstract class DslTest[RunF[_]: Monad: Compute: Bracket[*[_], Throwable]: MeasureTest: TestFramework[*[_], FR], FR]
38 | extends TestBase[RunF, FR]
39 | {
40 | private[this] val testsDsl: DslTests[RunF, FR] =
41 | DslTests.cons
42 |
43 | def tests: Suite[RunF, Unit, Unit] =
44 | testsDsl.tests.toList match {
45 | case head :: tail =>
46 | Suite.sequential(head, tail: _*)
47 | case Nil =>
48 | Suite.Pure(())
49 | }
50 |
51 | private[this] case class Add(desc: String)
52 | extends TestAdder[RunF, Id, Const[Unit, *]]
53 | {
54 | def apply[A](thunk: Id[RunF[KlkResult[A]]]): Const[Unit, A] = {
55 | testsDsl.plain(KlkTest.plain(desc)(thunk))
56 | Const(())
57 | }
58 | }
59 |
60 | def test(desc: String): TestBuilder[RunF, HNil, Id, Const[Unit, *]] =
61 | TestBuilder(TestResources.empty)(Add(desc))
62 |
63 | def sharedResource[R]
64 | (resource: Resource[RunF, R])
65 | : DslSharedResource[RunF, R] = {
66 | val res = DslSharedResource.cons[RunF, R]
67 | testsDsl.resource(resource, res)
68 | res
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/TestBuilder.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Functor
4 | import cats.effect.Resource
5 | import shapeless.{::, HList, HNil}
6 |
7 | case class TestResources[ResParams <: HList](resources: ResParams)
8 |
9 | object TestResources
10 | {
11 | def empty: TestResources[HNil] =
12 | TestResources(HNil)
13 | }
14 |
15 | case class ConsTest[RunF[_], TestRes <: HList, TestShape[_]]
16 | (resources: TestResources[TestRes])
17 | {
18 | def apply[Params, Thunk, Value]
19 | (thunk: TestShape[Thunk])
20 | (implicit transform: TransformTestThunk.Aux[RunF, TestRes, Params, Thunk, Value], functor: Functor[TestShape])
21 | : TestShape[RunF[KlkResult[Value]]] =
22 | thunk.map(transform(resources)(_))
23 | }
24 |
25 | trait TestAdder[RunF[_], TestShape[_], AddResult[_]]
26 | {
27 | def apply[A](test: TestShape[RunF[KlkResult[A]]]): AddResult[A]
28 | }
29 |
30 | case class AddTest[RunF[_], TestRes <: HList, Params, TestShape[_], AddResult[_]]
31 | (cons: ConsTest[RunF, TestRes, TestShape])
32 | (add: TestAdder[RunF, TestShape, AddResult])
33 | {
34 | def apply[Thunk, Value]
35 | (thunk: TestShape[Thunk])
36 | (implicit transform: TransformTestThunk.Aux[RunF, TestRes, Params, Thunk, Value], functor: Functor[TestShape])
37 | : AddResult[Value] =
38 | add(cons(thunk))
39 | }
40 |
41 | case class TestBuilder[RunF[_], TestRes <: HList, TestShape[_], AddResult[_]]
42 | (resources: TestResources[TestRes])
43 | (add: TestAdder[RunF, TestShape, AddResult])
44 | {
45 | def adder[Params, Output]: AddTest[RunF, TestRes, Params, TestShape, AddResult] =
46 | AddTest(ConsTest[RunF, TestRes, TestShape](resources))(add)
47 |
48 | def apply[Thunk, Value]
49 | (thunk: TestShape[Thunk])
50 | (implicit transform: TransformTestThunk.Aux[RunF, TestRes, NoExecutionParams, Thunk, Value], functor: Functor[TestShape])
51 | : AddResult[Value] =
52 | adder(thunk)
53 |
54 | def forallNoShrink: AddTest[RunF, TestRes, PropTrans.Full, TestShape, AddResult] =
55 | adder
56 |
57 | def forall: AddTest[RunF, TestRes, PropTrans.Shrink, TestShape, AddResult] =
58 | adder
59 |
60 | def laws: AddTest[RunF, TestRes, LawsParams, TestShape, AddResult] =
61 | adder
62 |
63 | def resource[R]
64 | (res: Resource[RunF, R])
65 | : TestBuilder[RunF, Resource[RunF, R] :: TestRes, TestShape, AddResult] =
66 | TestBuilder(TestResources(res :: resources.resources))(add)
67 | }
68 |
69 | object TestBuilder
70 | {
71 | def cons[RunF[_], TestShape[_], AddResult[_]]
72 | (add: TestAdder[RunF, TestShape, AddResult])
73 | : TestBuilder[RunF, HNil, TestShape, AddResult] =
74 | TestBuilder(TestResources.empty)(add)
75 | }
76 |
--------------------------------------------------------------------------------
/core/src/test/scala/klk/KlkSpecification.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.Id
4 | import cats.effect.{Resource, Sync}
5 | import org.specs2.matcher.MatchResult
6 | import org.specs2.mutable.Specification
7 | import org.specs2.specification.core.Fragment
8 | import shapeless.HNil
9 |
10 | abstract class KlkSpecification[RunF[_]: Sync: Compute: TestFramework[*[_], NoopResources.type]]
11 | extends Specification
12 | {
13 | private[this] case class Add()
14 | extends TestAdder[RunF, Id, λ[a => RunF[KlkResult[a]]]]
15 | {
16 | def apply[A](thunk: Id[RunF[KlkResult[A]]]): RunF[KlkResult[A]] = {
17 | thunk
18 | }
19 | }
20 |
21 | def test[A]
22 | (f: TestBuilder[RunF, HNil, Id, λ[a => RunF[KlkResult[a]]]] => RunF[KlkResult[Unit]])
23 | : KlkResult[Unit] = {
24 | val th: RunF[KlkResult[Unit]] =
25 | // TODO 2.12 compat
26 | f(TestBuilder.cons[RunF, Id, λ[a => RunF[KlkResult[a]]]](Add()))
27 | val kt = KlkTest.plain("test")(th)
28 | KlkTest.runPlain(kt)(NoopResources)
29 | }
30 |
31 | def assertWith
32 | (desc: String)
33 | (f: TestBuilder[RunF, HNil, Id, λ[a => RunF[KlkResult[a]]]] => RunF[KlkResult[Unit]])
34 | (check: KlkResult[Unit] => MatchResult[Any])
35 | : Fragment =
36 | desc >> test(f).must(check)
37 |
38 | def assert
39 | (desc: String)
40 | (f: TestBuilder[RunF, HNil, Id, λ[a => RunF[KlkResult[a]]]] => RunF[KlkResult[Unit]])
41 | (target: KlkResult[Unit])
42 | : Fragment =
43 | assertWith(desc)(f)(_ === target)
44 | }
45 |
46 | abstract class KlkSharedResourceSpecification[RunF[_]: Sync: Compute: TestFramework[*[_], NoopResources.type], R]
47 | extends Specification
48 | {
49 | def resource: Resource[RunF, R]
50 |
51 | private[this] case class Add()
52 | extends TestAdder[RunF, R => *, λ[a => R => RunF[KlkResult[a]]]]
53 | {
54 | def apply[A](thunk: R => RunF[KlkResult[A]]): R => RunF[KlkResult[A]] = {
55 | thunk
56 | }
57 | }
58 |
59 | def test
60 | (f: TestBuilder[RunF, HNil, R => *, λ[a => R => RunF[KlkResult[a]]]] => List[R => RunF[KlkResult[Unit]]])
61 | : KlkResult[Unit] = {
62 | val sr = DslSharedResource.cons[RunF, R]
63 | // TODO 2.12 compat
64 | val thunks: List[R => RunF[KlkResult[Unit]]] =
65 | f(TestBuilder.cons[RunF, R => *, λ[a => R => RunF[KlkResult[a]]]](Add()))
66 | val kt = thunks.map(th => KlkTest.cons("test")(th))
67 | KlkTest.runResource(resource)(kt)(NoopResources)
68 | }
69 |
70 | def assert
71 | (desc: String)
72 | (f: TestBuilder[RunF, HNil, R => *, λ[a => R => RunF[KlkResult[a]]]] => List[R => RunF[KlkResult[Unit]]])
73 | (target: KlkResult[Unit])
74 | : Fragment =
75 | desc >> test(f).must_==(target)
76 | }
77 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/EvalSuite.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.{Functor, Monad, Reducible}
4 | import cats.data.{Kleisli, OptionT, WriterT}
5 |
6 | object EvalSuite
7 | {
8 | type State[A] = (List[TestStats], Option[A])
9 | type K[F[_], A] = Kleisli[F, RunTestResources[F], A]
10 | type M[F[_], A] = OptionT[WriterT[K[F, *], List[TestStats], *], A]
11 |
12 | def lift[F[_], A](fa: K[F, State[A]]): M[F, A] =
13 | OptionT(WriterT(fa))
14 |
15 | def unless[F[_]: Monad, A](condition: M[F, A])(alternative: M[F, A]): M[F, A] =
16 | lift(
17 | condition.value.run.flatMap {
18 | case (stats, Some(a)) =>
19 | Kleisli.pure((stats, Some(a)))
20 | case (stats, None) =>
21 | alternative.value.run.map {
22 | case (newStats, Some(a)) =>
23 | (stats.map(TestStats.recover) ++ newStats, Some(a))
24 | case (newStats, None) =>
25 | (newStats ++ stats, None)
26 | }
27 | }
28 | )
29 |
30 | def liftTest[F[_]: Monad, A](fa: K[F, Suite.Output[A]]): M[F, A] =
31 | lift(fa.flatMapF(output => (output.stats, Suite.Output.toOption(output)).pure[F]))
32 |
33 | def combineResults[A](x: State[A], y: State[A]): State[A] =
34 | (x, y) match {
35 | case ((statsX, valueX), (statsY, valueY)) =>
36 | (statsX |+| statsY, valueX <* valueY)
37 | }
38 |
39 | def foldSuites[F[_]: Functor, T[_]: Reducible, A](fa: F[T[State[A]]]): F[State[A]] =
40 | fa.map(_.reduceLeft(combineResults))
41 |
42 | def step[F[_]: Monad, Res, A]
43 | (res: Res)
44 | : Suite[F, Res, A] => M[F, A] = {
45 | case Suite.Pure(a) =>
46 | OptionT.pure(a)
47 | case Suite.Suspend(test) =>
48 | liftTest(Kleisli(test(res))).widen
49 | case suite @ Suite.SharedResource(resource, test) =>
50 | import suite.bracket
51 | lift(Kleisli(testRes => resource.use(r => evalF(r)(test).run(testRes))))
52 | case Suite.If(head, tail) =>
53 | eval(res)(head) >>= tail.andThen(eval(res))
54 | case Suite.Unless(head, tail) =>
55 | unless(eval(res)(head))(eval(res)(tail))
56 | case Suite.Sequential(tests) =>
57 | OptionT(WriterT(foldSuites(tests.traverse(evalF(res)))))
58 | case suite @ Suite.Parallel(tests) =>
59 | import suite.parallel
60 | tests.parTraverse(eval(res)).map(_.head)
61 | }
62 |
63 | def eval[F[_]: Monad, Res, A]
64 | (res: Res)
65 | (test: Suite[F, Res, A])
66 | : M[F, A] =
67 | step[F, Res, A](res).apply(test)
68 |
69 | def evalF[F[_]: Monad, Res, A]
70 | (res: Res)
71 | (test: Suite[F, Res, A])
72 | : K[F, State[A]] =
73 | eval(res)(test).value.run
74 |
75 | def apply[F[_]: Monad, A]
76 | (tests: Suite[F, Unit, A])
77 | : Kleisli[F, RunTestResources[F], List[TestStats]] =
78 | eval(())(tests).value.written
79 | }
80 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/Laws.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.{Applicative, Functor}
4 | import cats.data.{Kleisli, NonEmptyList}
5 | import org.scalacheck.Prop
6 | import org.typelevel.discipline.Laws
7 |
8 | final class LawsParams
9 |
10 | object LawsParams
11 | {
12 | implicit def PropThunk_Output[F0[_]: Functor]
13 | : PropThunk.Aux[F0[Prop], LawsParams, F0] =
14 | new PropThunk[F0[Prop], LawsParams] {
15 | type F[A] = F0[A]
16 | def apply(f: F[Prop]): PropertyTest[F] =
17 | PropertyTest(Kleisli(params => f.map(out => out(params))))
18 | }
19 | }
20 |
21 | sealed trait LawsResult
22 |
23 | object LawsResult
24 | {
25 | case object Empty
26 | extends LawsResult
27 |
28 | case class Executed(results: NonEmptyList[PropertyTestResult])
29 | extends LawsResult
30 |
31 | def empty: LawsResult =
32 | Empty
33 |
34 | def klkResult: LawsResult => KlkResult[Unit] = {
35 | case Executed(results) =>
36 | KlkResult.Multi(results.map(PropertyTestResult.klkResult))
37 | case Empty =>
38 | KlkResult.failure(KlkResult.Details.Simple(NonEmptyList.one("no properties in law test")))
39 | }
40 |
41 | implicit def TestResult_LawsResult: TestResult.Aux[LawsResult, Unit] =
42 | new TestResult[LawsResult] {
43 | type Value = Unit
44 |
45 | def apply(result: LawsResult): KlkResult[Unit] =
46 | klkResult(result)
47 | }
48 | }
49 |
50 | trait FunctorialLaws[Class[_[A]], Subject[_]]
51 |
52 | object LawsTest
53 | {
54 | def rule[F[_]: Applicative](propRun: PropRun.Aux[F[Prop], LawsParams, F])(prop: Prop): F[PropertyTestResult] =
55 | PropRun(propRun)(prop.pure[F])
56 |
57 | def apply[F[_]: Applicative, L <: Laws]
58 | (propRun: PropRun.Aux[F[Prop], LawsParams, F])
59 | (rules: L#RuleSet)
60 | : F[LawsResult] =
61 | rules.all.properties.toList.map(_._2) match {
62 | case head :: tail =>
63 | NonEmptyList(head, tail).traverse(rule[F](propRun)).map(LawsResult.Executed(_))
64 | case Nil =>
65 | LawsResult.empty.pure[F]
66 | }
67 | }
68 |
69 | // object LawsTest
70 | // {
71 | // // def rule[F[_]: Applicative](propRun: PropRun.Aux[F[Prop], PropTrans.Shrink, F])(prop: Prop): F[PropertyTestResult] =
72 | // // PropRun(propRun)(prop.pure[F])
73 |
74 | // def propResult(result: Prop.Result): PropertyTestResult =
75 | // PropertyTestResult(
76 | // PropertyTestResult.success(result.status),
77 | // PropertyTestState.Stats(true, 0, 0),
78 | // PropTest.Result(PropTest.Status)
79 | // )
80 |
81 | // def apply[F[_]: Sync, L <: Laws]
82 | // (rules: L#RuleSet)
83 | // : F[LawsResult] =
84 | // rules.all.properties.toList.map(_._2) match {
85 | // case head :: tail =>
86 | // NonEmptyList(head, tail)
87 | // .traverse(a => Sync[F].delay(a(Gen.Parameters.default)))
88 | // .map(_.map(propResult))
89 | // .map(LawsResult.Executed(_))
90 | // case Nil =>
91 | // LawsResult.empty.pure[F]
92 | // }
93 | // }
94 |
--------------------------------------------------------------------------------
/http4s/src/main/scala/klk/Http4s.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import java.net.ServerSocket
4 |
5 | import cats.{Functor, MonadError}
6 | import cats.data.{EitherT, NonEmptyList}
7 | import cats.effect.{Resource, Sync}
8 | import org.http4s.{EntityDecoder, HttpApp, HttpRoutes, Request, Response, Status, Uri}
9 | import org.http4s.client.Client
10 | import org.http4s.client.blaze.BlazeClientBuilder
11 | import org.http4s.implicits._
12 | import org.http4s.server.blaze.BlazeServerBuilder
13 |
14 | case class Port(number: Int)
15 |
16 | object FreePort
17 | {
18 | def find[F[_]: Sync]: F[Port] =
19 | Sync[F].bracket(Sync[F].delay(new ServerSocket(0))) { socket =>
20 | Sync[F].delay {
21 | socket.setReuseAddress(true)
22 | Port(socket.getLocalPort)
23 | }
24 | }(a => Sync[F].delay(a.close()))
25 | }
26 |
27 | case class TestClient[F[_]](client: Client[F], port: Port)
28 | {
29 | def authority: Uri.Authority =
30 | Uri.Authority(port = Some(port.number))
31 |
32 | def uri(uri: Uri): Uri =
33 | uri.copy(authority = Some(authority))
34 |
35 | def withUri(request: Request[F]): Request[F] =
36 | request.withUri(uri(request.uri))
37 |
38 | def fetch[A](request: Request[F])(f: Response[F] => F[A]): F[A] =
39 | client.fetch(withUri(request))(f)
40 |
41 | def success[A]
42 | (request: Request[F])
43 | (f: Response[F] => F[A])
44 | (implicit monadError: MonadError[F, Throwable], decoder: EntityDecoder[F, String])
45 | : EitherT[F, KlkResult[Unit], A] =
46 | EitherT {
47 | fetch(request) {
48 | case Status.Successful(response) =>
49 | f(response).map(Right(_))
50 | case response =>
51 | response.as[String]
52 | .map(body => Left(KlkResult.simpleFailure(NonEmptyList.one(s"request failed: $body ($response)"))))
53 | }
54 | }
55 | }
56 |
57 | object Http4s
58 | {
59 | case class ServeRoutes[RunF[_], FR]
60 | (app: HttpApp[RunF], builder: Option[BlazeServerBuilder[RunF]], client: Option[Resource[RunF, Client[RunF]]])
61 | {
62 | def withBuilder(newBuilder: BlazeServerBuilder[RunF]): ServeRoutes[RunF, FR] =
63 | copy(builder = Some(newBuilder))
64 |
65 | def test[A]
66 | (tests: SharedResource[RunF, TestClient[RunF], FR] => Suite[RunF, TestClient[RunF], A])
67 | (
68 | implicit
69 | sync: Sync[RunF],
70 | compute: Compute[RunF],
71 | framework: TestFramework[RunF, FR],
72 | consConcurrent: ConsConcurrent[RunF],
73 | consTimer: ConsTimer[RunF],
74 | )
75 | : Suite[RunF, Unit, A] =
76 | SharedResource.suite(Http4s.serverResource[RunF](consConcurrent, consTimer)(app, builder, client))(tests)
77 | }
78 |
79 | case class Serve[RunF[_], FR]()
80 | {
81 | def app
82 | (app: HttpApp[RunF])
83 | : ServeRoutes[RunF, FR] =
84 | ServeRoutes(app, None, None)
85 |
86 | def routes
87 | (r: HttpRoutes[RunF])
88 | (implicit functor: Functor[RunF])
89 | : ServeRoutes[RunF, FR] =
90 | app(r.orNotFound)
91 | }
92 |
93 | def server[RunF[_], FR]: Serve[RunF, FR] =
94 | Serve()
95 |
96 | def defaultClient[RunF[_]: Sync](consConcurrent: ConsConcurrent[RunF]): Resource[RunF, Client[RunF]] =
97 | Concurrency.fixedPoolEc.flatMap(ec => BlazeClientBuilder[RunF](ec)(consConcurrent(ec)).resource)
98 |
99 | def serverResource[RunF[_]: Sync]
100 | (consConcurrent: ConsConcurrent[RunF], consTimer: ConsTimer[RunF])
101 | (app: HttpApp[RunF], builder: Option[BlazeServerBuilder[RunF]], client: Option[Resource[RunF, Client[RunF]]])
102 | : Resource[RunF, TestClient[RunF]] =
103 | Concurrency.fixedPoolEc[RunF].flatMap { ec =>
104 | for {
105 | port <- Resource.liftF(FreePort.find[RunF])
106 | _ <- builder.getOrElse(BlazeServerBuilder[RunF](consConcurrent(ec), consTimer(ec)))
107 | .bindHttp(port.number, "0.0.0.0")
108 | .withHttpApp(app)
109 | .resource
110 | client <- client.getOrElse(defaultClient(consConcurrent))
111 | } yield TestClient(client, port)
112 | }
113 | }
114 |
115 | trait Http4s[RunF[_], FR]
116 | {
117 | def server
118 | : Http4s.Serve[RunF, FR] =
119 | Http4s.server[RunF, FR]
120 | }
121 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/KlkResult.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.{Functor, MonoidK}
4 | import cats.data.NonEmptyList
5 | import cats.kernel.Monoid
6 |
7 | sealed trait KlkResult[+A]
8 |
9 | object KlkResult
10 | extends KlkResultInstances
11 | {
12 | sealed trait Details
13 |
14 | object Details
15 | {
16 | case object NoDetails
17 | extends Details
18 |
19 | case class Simple(info: NonEmptyList[String])
20 | extends Details
21 |
22 | case class Complex(desc: NonEmptyList[String], target: String, actual: String)
23 | extends Details
24 | }
25 |
26 | case object Zero
27 | extends KlkResult[Nothing]
28 |
29 | case class Pure[A](a: A)
30 | extends KlkResult[A]
31 |
32 | case class Fatal(error: Throwable)
33 | extends KlkResult[Nothing]
34 |
35 | case class Single[A](value: A, success: Boolean, details: Details)
36 | extends KlkResult[A]
37 |
38 | case class Multi[A](results: NonEmptyList[KlkResult[A]])
39 | extends KlkResult[A]
40 |
41 | def apply(success: Boolean)(details: Details): KlkResult[Unit] =
42 | Single((), success, details)
43 |
44 | object Value
45 | {
46 | def unapply[A](result: KlkResult[A]): Option[A] =
47 | result match {
48 | case Pure(a) => Some(a)
49 | case Single(a, _, _) => Some(a)
50 | case Multi(results) => results.collectFirst { case Value(a) => a }
51 | case _ => None
52 | }
53 | }
54 |
55 | def success: Details => KlkResult[Unit] =
56 | apply(true)
57 |
58 | def failure: Details => KlkResult[Unit] =
59 | apply(false)
60 |
61 | def simpleFailure: NonEmptyList[String] => KlkResult[Unit] =
62 | failure.compose(Details.Simple)
63 |
64 | def valueFailure[A](value: A)(message: NonEmptyList[String]): KlkResult[A] =
65 | Single(value, false, Details.Simple(message))
66 |
67 | def bool(success: Boolean): KlkResult[Unit] =
68 | Single((), success, Details.NoDetails)
69 |
70 | def successful[A]: KlkResult[A] => Boolean = {
71 | case Zero => false
72 | case Single(_, s, _) => s
73 | case Multi(results) => results.filterNot(successful).isEmpty
74 | case Fatal(_) => false
75 | case Pure(_) => false
76 | }
77 |
78 | def list[A]: KlkResult[A] => List[KlkResult[A]] = {
79 | case Zero => Nil
80 | case Multi(results) => results.toList
81 | case a => List(a)
82 | }
83 |
84 | def combine[A]: KlkResult[A] => KlkResult[A] => KlkResult[A] = {
85 | case Zero => {
86 | case Zero => Zero
87 | case a @ Multi(_) => a
88 | case a => a
89 | }
90 | case Multi(NonEmptyList(h, t)) => b => Multi(NonEmptyList(h, t ++ list(b)))
91 | case a => b => Multi(NonEmptyList(a, list(b)))
92 | }
93 |
94 | def failures[A]: KlkResult[A] => List[Details] = {
95 | case Single(_, false, d) => List(d)
96 | case Multi(results) => results.toList.flatMap(failures)
97 | case _ => Nil
98 | }
99 |
100 | def details[A]: KlkResult[A] => List[Details] = {
101 | case Single(_, _, dt) => List(dt)
102 | case Multi(results) => results.toList >>= details[A]
103 | case _ => Nil
104 | }
105 | }
106 |
107 | private[klk] trait KlkResultInstances
108 | {
109 | implicit def Monoid_KlkResult[A]: Monoid[KlkResult[A]] =
110 | new Monoid[KlkResult[A]] {
111 | def empty: KlkResult[A] = KlkResult.Zero
112 | def combine(x: KlkResult[A], y: KlkResult[A]): KlkResult[A] = KlkResult.combine(x)(y)
113 | }
114 |
115 | implicit def MonoidK_KlkResult: MonoidK[KlkResult] =
116 | new MonoidK[KlkResult] {
117 | def empty[A]: KlkResult[A] = KlkResult.Zero
118 | def combineK[A](x: KlkResult[A], y: KlkResult[A]): KlkResult[A] = KlkResult.combine(x)(y)
119 | }
120 |
121 | implicit def Functor_KlkResult: Functor[KlkResult] =
122 | new Functor[KlkResult] {
123 | def map[A, B](fa: KlkResult[A])(f: A => B): KlkResult[B] =
124 | fa match {
125 | case KlkResult.Single(a, success, details) =>
126 | KlkResult.Single(f(a), success, details)
127 | case KlkResult.Multi(results) =>
128 | KlkResult.Multi(results.map(_.map(f)))
129 | case KlkResult.Zero =>
130 | KlkResult.Zero
131 | case KlkResult.Fatal(error) =>
132 | KlkResult.Fatal(error)
133 | case KlkResult.Pure(a) =>
134 | KlkResult.Pure(f(a))
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/sbt/src/main/scala/klk/KlkTask.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import java.lang.reflect.Constructor
4 |
5 | import sbt.testing.{
6 | Event,
7 | EventHandler,
8 | Fingerprint,
9 | Logger,
10 | OptionalThrowable,
11 | Selector,
12 | Status,
13 | SubclassFingerprint,
14 | Task,
15 | TaskDef,
16 | TestSelector
17 | }
18 |
19 | case class FinishEvent(
20 | status: Status,
21 | duration: Long,
22 | fingerprint: Fingerprint,
23 | fullyQualifiedName: String,
24 | selector: Selector,
25 | throwable: OptionalThrowable,
26 | )
27 | extends Event
28 |
29 | object FinishEvent
30 | {
31 | def cons(taskDef: TaskDef, name: String, status: Status, duration: Long): FinishEvent =
32 | FinishEvent(
33 | status,
34 | duration,
35 | taskDef.fingerprint,
36 | taskDef.fullyQualifiedName,
37 | new TestSelector(name),
38 | new OptionalThrowable,
39 | )
40 | }
41 |
42 | object ExecuteTests
43 | {
44 | def status(success: Boolean): Status =
45 | if (success) Status.Success else Status.Failure
46 |
47 | def sendResult(taskDef: TaskDef)(events: EventHandler)(result: TestStats): Unit =
48 | events.handle(FinishEvent.cons(
49 | taskDef,
50 | result.desc,
51 | status(TestStats.reportAsSuccess(result)),
52 | result.duration,
53 | ))
54 |
55 | def apply(taskDef: TaskDef)(test: FrameworkTest[SbtResources]): (EventHandler, Array[Logger]) => Array[Task] = {
56 | (events, log) => {
57 | test
58 | .run(SbtResources(SbtTestLog(log)))
59 | .foreach(ExecuteTests.sendResult(taskDef)(events))
60 | Array()
61 | }
62 | }
63 | }
64 |
65 | case class KlkTask(taskDef: TaskDef, exe: (EventHandler, Array[Logger]) => Array[Task], tags: Array[String])
66 | extends Task
67 | {
68 | def execute(handler: EventHandler, loggers: Array[Logger]): Array[Task] =
69 | exe(handler, loggers)
70 | }
71 |
72 | object KlkTask
73 | {
74 | case class Error(details: Error.Details, exception: Option[Throwable])
75 |
76 | object Error
77 | {
78 | sealed trait Details
79 |
80 | object Details
81 | {
82 | case class LoadClass(name: String)
83 | extends Details
84 |
85 | case class CastClass(name: String, cls: Class[_])
86 | extends Details
87 |
88 | case class Ctors(cls: Class[_])
89 | extends Details
90 |
91 | case class NoCtor(cls: Class[_])
92 | extends Details
93 |
94 | case class CastCtor(ctor: Constructor[_])
95 | extends Details
96 |
97 | case class SetAccessible(ctor: Constructor[_])
98 | extends Details
99 |
100 | case class Instantiate(ctor: Constructor[_])
101 | extends Details
102 | }
103 | }
104 |
105 | def fromTest(taskDef: TaskDef)(test: FrameworkTest[SbtResources]): KlkTask =
106 | KlkTask(taskDef, ExecuteTests(taskDef)(test), Array.empty)
107 |
108 | def classNameSuffix: Fingerprint => String = {
109 | case a: SubclassFingerprint if a.isModule => "$"
110 | case _ => ""
111 | }
112 |
113 | def className(taskDef: TaskDef): String =
114 | taskDef.fullyQualifiedName + classNameSuffix(taskDef.fingerprint)
115 |
116 | def safe[A](error: Error.Details)(f: => A): Either[Error, A] =
117 | Either.catchOnly[Throwable](f).leftMap(e => Error(error, Some(e)))
118 |
119 | def loadClass(loader: ClassLoader)(name: String): Either[Error, Class[FrameworkTest[SbtResources]]] =
120 | for {
121 | cls <- safe(Error.Details.LoadClass(name))(loader.loadClass(name))
122 | cast <- safe(Error.Details.CastClass(name, cls))(cls.asInstanceOf[Class[FrameworkTest[SbtResources]]])
123 | } yield cast
124 |
125 | def findCtor(cls: Class[FrameworkTest[SbtResources]]): Either[Error, Constructor[FrameworkTest[SbtResources]]] =
126 | for {
127 | ctors <- safe(Error.Details.Ctors(cls))(cls.getDeclaredConstructors)
128 | ctor <- Either.fromOption(ctors.headOption, Error(Error.Details.NoCtor(cls), None))
129 | _ <- safe(Error.Details.SetAccessible(ctor))(ctor.setAccessible(true))
130 | cast <- safe(Error.Details.CastCtor(ctor))(ctor.asInstanceOf[Constructor[FrameworkTest[SbtResources]]])
131 | } yield cast
132 |
133 | def fromTaskDef(loader: ClassLoader)(taskDef: TaskDef): Either[Error, KlkTask] =
134 | for {
135 | cls <- loadClass(loader)(className(taskDef))
136 | ctor <- findCtor(cls)
137 | inst <- safe(Error.Details.Instantiate(ctor))(ctor.newInstance())
138 | } yield fromTest(taskDef)(inst)
139 |
140 | }
141 |
142 | object KlkTasks
143 | {
144 | def error: KlkTask.Error.Details => String = {
145 | case KlkTask.Error.Details.LoadClass(name) =>
146 | s"could not load class $name:"
147 | case KlkTask.Error.Details.CastClass(name, cls) =>
148 | s"could not cast class $name ($cls) to FrameworkTest:"
149 | case KlkTask.Error.Details.Ctors(cls) =>
150 | s"error when getting ctors for $cls:"
151 | case KlkTask.Error.Details.NoCtor(cls) =>
152 | s"class $cls has no ctors"
153 | case KlkTask.Error.Details.CastCtor(ctor) =>
154 | s"could not cast ctor $ctor:"
155 | case KlkTask.Error.Details.SetAccessible(ctor) =>
156 | s"could not make ctor $ctor accessible:"
157 | case KlkTask.Error.Details.Instantiate(ctor) =>
158 | s"could not instantiate constructor $ctor:"
159 | }
160 |
161 | def logError(loggers: Array[Logger])(lines: List[String]): Array[Task] = {
162 | SbtTestLog.unsafe(SbtTestLog(loggers))(_.error)(lines)
163 | Array.empty
164 | }
165 |
166 | def taskImpl: KlkTask.Error => (EventHandler, Array[Logger]) => Array[Task] = {
167 | case KlkTask.Error(details, exception) =>
168 | (_, loggers) =>
169 | logError(loggers)(error(details) :: exception.toList.map(_.getMessage))
170 | }
171 |
172 | def errorTask(taskDef: TaskDef)(error: KlkTask.Error): Task =
173 | KlkTask(taskDef, taskImpl(error), Array.empty)
174 |
175 | def processTaskDef(testClassLoader: ClassLoader)(taskDef: TaskDef): Task =
176 | KlkTask.fromTaskDef(testClassLoader)(taskDef).valueOr(errorTask(taskDef))
177 |
178 | def process(testClassLoader: ClassLoader)(taskDefs: Array[TaskDef]): Array[Task] =
179 | taskDefs.toList.map(processTaskDef(testClassLoader)).toArray
180 | }
181 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Intro
2 |
3 | **kallikrein** is a Scala testing framework for sbt focused on running [cats-effect] based programs.
4 |
5 | If you're into matcher DSLs, check out [xpct], which is a framework-agnostic typed matcher lib with support for
6 | **kallikrein**.
7 |
8 | ## module ids
9 |
10 | ```sbt
11 | "io.tryp" %% "kallikrein-sbt" % "0.5.2"
12 | "io.tryp" %% "kallikrein-http4s-sbt" % "0.5.2"
13 | ```
14 |
15 | ## sbt
16 |
17 | To use the framework in a project, specify the setting:
18 |
19 | ```sbt
20 | testFrameworks += new TestFramework("klk.KlkFramework")
21 | ```
22 |
23 | # Basics
24 |
25 | ## Imperative DSL
26 |
27 | ```scala
28 | class SomeTest
29 | extends klk.IOTest
30 | {
31 | test("description")(IO.pure(1 == 1))
32 | }
33 | ```
34 |
35 | Tests are added by calling the `test` function in a class inheriting `klk.SimpleTest[F]`, where `F[_]` is an effect that
36 | implements `cats.effect.Sync`.
37 | `klk.IOTest` is a convenience trait using `cats.effect.IO`.
38 |
39 | Assertions are returned from the test thunk and can be anything, as long as there is an instance for `klk.TestResult`.
40 | The internal type representing the result is `KlkResult`.
41 |
42 | ## Composable Tests
43 |
44 | The above mentioned `test` builder can also be used in a pure context and has a nice arsenal of typeclass instances for
45 | composition.
46 |
47 | When tests are sequenced in a for comprehension, the semantic effect is that of conditional execution:
48 | If one test fails, all following tests are skipped.
49 |
50 | There is an instance of `SemigroupK` available, allowing you to use the `<+>` operator, resulting in the alternative, or
51 | `unless`, semantics – i.e. if and only if the first test fails, execute the second one and use its result.
52 |
53 | For independent tests, there are two combinators: `sequential` and `parallel`.
54 | They do what you would expect, similar to the imperative test building syntax.
55 | The `parallel` variant requires an instances of `cats.Parallel` and will execute the tests with a `parTraverse`.
56 |
57 | The following example will result in a successful end result:
58 |
59 | ```scala
60 | class DepTest
61 | extends ComposeTest[IO, SbtResources]
62 | {
63 | def testSuccess: IO[Boolean] =
64 | IO.pure(true)
65 |
66 | def testFail: IO[Boolean] =
67 | IO.raiseError(new Exception("boom"))
68 |
69 | implicit def cs: ContextShift[IO] =
70 | IO.contextShift(ExecutionContext.global)
71 |
72 | def tests: Suite[IO, Unit, Unit] =
73 | for {
74 | _ <- sharedResource(Resource.pure(5))(
75 | builder =>
76 | builder.test("five is 4")(five => IO.pure(five == 4)) <+>
77 | builder.test("five is 5")(five => IO.pure(five == 5))
78 | )
79 | _ <- test("test 1")(testSuccess)
80 | _ <- test("test 2")(testFail) <+> test("test 3")(testSuccess)
81 | _ <- Suite.parallel(test("test 4a")(testSuccess), test("test 4b")(testSuccess)) <+> test("test 5")(testFail)
82 | _ <- test("test 7")(testSuccess)
83 | } yield ()
84 | }
85 | ```
86 |
87 | # Results and Effects
88 |
89 | The effect type of an individual test can be different from the main effect if there is an instance of `klk.Compile[F,
90 | G]`.
91 | For example, `EitherT` is supported out of the box:
92 |
93 | ```scala
94 | test("EitherT")(EitherT.right[Unit](IO.pure(1 == 1)))
95 | ```
96 |
97 | A `Left` value will be converted into a failure by the typeclass `TestResult[A]`, meaning that this works just as well with
98 | `IO[Either[A, B]]`.
99 |
100 | ## fs2
101 |
102 | A `Stream[F, A]` will automatically be compiled to `F`, with the inner value being handled by a dependent typeclass
103 | instance.
104 |
105 | ## http4s
106 |
107 | The `kallikrein-http4s-sbt` module provides a [shared resource](#resources) that runs an [http4s] server on a random
108 | port and supplies tests with a client and the `Uri` of the server wrapped in a test client interface:
109 |
110 | ```scala
111 | class SomeTest
112 | extends klk.Http4sTest[IO]
113 | {
114 | def tests: Suite[IO, Unit, Unit] =
115 | server
116 | .app(HttpApp.liftF(IO.pure(Response[IO]())))
117 | .test { builder =>
118 | builder.test("http4s") { client =>
119 | client.fetch(Request[IO]())(_ => IO.pure(true))
120 | }
121 | }
122 | }
123 | ```
124 |
125 | The `TestClient` class provides a `fetch` method that injects the uri into the request with its
126 | `withUri(request: Request[F])` method, as well as a `success` method, which produces a failed `KlkResult` if the
127 | response status is other than `2xx`.
128 |
129 | # Resources
130 |
131 | Tests can depend on shared and individual resources that will be supplied by the framework when running:
132 |
133 | ```scala
134 | class SomeTest
135 | extends klk.IOTest
136 | {
137 | val res1: Resource[IO, Int] = Resource.pure(1)
138 |
139 | val res2: Resource[IO, Int] = Resource.pure(1)
140 |
141 | test("resource").resource(res1).resource(res2)((i: Int) => (j: Int) => IO.pure(i == j))
142 |
143 | def eightySix: SharedResource[IO, Int] =
144 | sharedResource(Resource.pure(86))
145 |
146 | eightySix.test("shared resource 1")(i => IO.pure(i == 86))
147 |
148 | eightySix.test("shared resource 2")(i => IO.pure(i == 68))
149 | }
150 | ```
151 |
152 | The shared resource will be acquired only once and supplied to all tests that use it.
153 |
154 | # Property Testing
155 |
156 | [Scalacheck] can be used in a test by calling the `forall` method on a test builder:
157 |
158 | ```scala
159 | class SomeTest
160 | extends klk.IOTest
161 | {
162 | test("are all lists of integers shorter than 5 elements?").forall((l1: List[Int]) => IO(l1.size < 5))
163 | }
164 | ```
165 |
166 | This features a custom runner for the properties built on fs2.
167 |
168 | A second variant `forallNoShrink` does what it advertises.
169 |
170 | # Law Checking
171 |
172 | [cats-discipline] laws can be checked like this:
173 |
174 | ```scala
175 | class SomeTest
176 | extends klk.IOTest
177 | {
178 | test("laws").laws(IO.pure(FunctorTests[List].functor[Int, Int, Int]))
179 | }
180 | ```
181 |
182 | [cats-effect]: https://github.com/typelevel/cats-effect
183 | [xpct]: https://github.com/tek/xpct
184 | [scalacheck]: https://github.com/typelevel/scalacheck
185 | [cats-discipline]: https://github.com/typelevel/discipline
186 | [http4s]: https://github.com/http4s/http4s
187 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/Suite.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import cats.{Functor, Monad, SemigroupK, StackSafeMonad}
4 | import cats.data.NonEmptyList
5 | import cats.effect.{Bracket, Resource}
6 | import cats.kernel.Monoid
7 |
8 | case class TestStats(
9 | desc: String,
10 | result: List[KlkResult.Details],
11 | success: Boolean,
12 | recovered: Boolean,
13 | duration: Long,
14 | )
15 |
16 | object TestStats
17 | {
18 | def recover(stats: TestStats): TestStats =
19 | stats.copy(recovered = true)
20 |
21 | def reportAsSuccess(stats: TestStats): Boolean =
22 | stats.success || stats.recovered
23 | }
24 |
25 | case class RunTestResources[F[_]](reporter: TestReporter[F], measure: MeasureTest[F])
26 |
27 | object RunTestResources
28 | {
29 | final class RTRCons[F[_]] {
30 | def apply[FR](res: FR)
31 | (implicit framework: TestFramework[F, FR], measure: MeasureTest[F])
32 | : RunTestResources[F] =
33 | RunTestResources(framework.reporter(res), measure)
34 | }
35 |
36 | def cons[F[_]]: RTRCons[F] =
37 | new RTRCons[F]
38 | }
39 |
40 | sealed trait Suite[F[_], Res, +A]
41 |
42 | object Suite
43 | {
44 | case class Output[+A](details: Output.Details[A], continue: Boolean, stats: List[TestStats])
45 |
46 | object Output
47 | {
48 | sealed trait Details[+A]
49 |
50 | object Details
51 | {
52 | case class Value[A](a: A)
53 | extends Details[A]
54 |
55 | case object Zero
56 | extends Details[Nothing]
57 |
58 | def zero[A]: Details[A] =
59 | Zero
60 |
61 | def pure[A](a: A): Details[A] =
62 | Value(a)
63 |
64 | implicit def Monoid_Details[A]: Monoid[Details[A]] =
65 | new Monoid[Details[A]] {
66 | def combine(x: Details[A], y: Details[A]): Details[A] =
67 | (x, y) match {
68 | case (_, Value(a)) => Value(a)
69 | case (Value(a), Zero) => Value(a)
70 | case (Zero, Zero) => Zero
71 | }
72 |
73 | def empty: Details[A] =
74 | zero
75 | }
76 |
77 | def fromResult[A]: KlkResult[A] => Details[A] = {
78 | case KlkResult.Value(a) => Value(a)
79 | case _ => Zero
80 | }
81 |
82 | def fromOption[A]: Option[A] => Details[A] = {
83 | case Some(a) => Value(a)
84 | case None => Zero
85 | }
86 |
87 | def successful[A]: Details[A] => Boolean = {
88 | case Value(_) => true
89 | case Zero => false
90 | }
91 | }
92 |
93 | def empty[A]: Output[A] =
94 | Output(Details.Zero, true, Nil)
95 |
96 | def pure[A](a: A): Output[A] =
97 | Output(Details.Value(a), true, Nil)
98 |
99 | implicit def Monoid_Output[A]: Monoid[Output[A]] =
100 | new Monoid[Output[A]] {
101 | def combine(x: Output[A], y: Output[A]): Output[A] =
102 | Output(x.details |+| y.details, x.continue && y.continue, x.stats |+| y.stats)
103 |
104 | def empty: Output[A] =
105 | Output.empty
106 | }
107 |
108 | object Value
109 | {
110 | def unapply[A](output: Output[A]): Option[A] =
111 | output.details match {
112 | case Details.Value(a) => Some(a)
113 | case _ => None
114 | }
115 | }
116 |
117 | def toOption[A]: Output[A] => Option[A] = {
118 | case Output(Details.Value(a), true, _) => Some(a)
119 | case _ => None
120 | }
121 | }
122 |
123 | case class Pure[F[_], Res, A](a: A)
124 | extends Suite[F, Res, A]
125 |
126 | case class Suspend[F[_], Res, A](test: Res => RunTestResources[F] => F[Output[A]])
127 | extends Suite[F, Res, A]
128 |
129 | case class SharedResource[F[_], Res, R, A](resource: Resource[F, R], suite: Suite[F, R, A])(
130 | implicit val bracket: Bracket[F, Throwable])
131 | extends Suite[F, Res, A]
132 |
133 | case class If[F[_], Res, A, B](head: Suite[F, Res, A], tail: A => Suite[F, Res, B])
134 | extends Suite[F, Res, B]
135 |
136 | case class Unless[F[_], Res, A](head: Suite[F, Res, A], tail: Suite[F, Res, A])
137 | extends Suite[F, Res, A]
138 |
139 | case class Sequential[F[_], Res, A](tests: NonEmptyList[Suite[F, Res, A]])
140 | extends Suite[F, Res, A]
141 |
142 | case class Parallel[F[_], Res, A](tests: NonEmptyList[Suite[F, Res, A]])(implicit val parallel: cats.Parallel[F])
143 | extends Suite[F, Res, A]
144 |
145 | def runTest[F[_]: Functor, Res, A]
146 | (resource: Res)
147 | (test: KlkTest[F, Res, A])
148 | (testRes: RunTestResources[F])
149 | : F[Output[A]] =
150 | testRes.measure(test.thunk(testRes.reporter)(resource))
151 | .map {
152 | case (result, duration) =>
153 | val success = KlkResult.successful(result)
154 | Output(
155 | Output.Details.fromResult(result),
156 | success,
157 | List(TestStats(test.desc, KlkResult.details(result), success, false, duration)),
158 | )
159 | }
160 |
161 | def single[F[_]: Functor, Res, A](test: KlkTest[F, Res, A]): Suite[F, Res, A] =
162 | Suspend(res => testRes => runTest(res)(test)(testRes))
163 |
164 | def resource[F[_], R, A]
165 | (resource: Resource[F, R], thunk: Suite[F, R, A])
166 | (implicit bracket: Bracket[F, Throwable])
167 | : Suite[F, Unit, A] =
168 | SharedResource(resource, thunk)
169 |
170 | def parallel[F[_]: cats.Parallel, Res, A](head: Suite[F, Res, A], tail: Suite[F, Res, A]*)
171 | : Suite[F, Res, A] =
172 | Parallel(NonEmptyList(head, tail.toList))
173 |
174 | def sequential[F[_], Res, A](head: Suite[F, Res, A], tail: Suite[F, Res, A]*): Suite[F, Res, A] =
175 | Sequential(NonEmptyList(head, tail.toList))
176 |
177 | def depend[F[_], Res](head: Suite[F, Res, Unit])(tail: Suite[F, Res, Unit]): Suite[F, Res, Unit] =
178 | If[F, Res, Unit, Unit](head, _ => tail)
179 |
180 | def unless[F[_], Res](head: Suite[F, Res, Unit])(tail: Suite[F, Res, Unit]): Suite[F, Res, Unit] =
181 | Unless(head, tail)
182 |
183 | implicit def Instances_Suite[F[_], Res]
184 | : Monad[Suite[F, Res, *]] with SemigroupK[Suite[F, Res, *]] =
185 | new SuiteInstances[F, Res]
186 | }
187 |
188 | class SuiteInstances[F[_], Res]
189 | extends StackSafeMonad[Suite[F, Res, *]]
190 | with SemigroupK[Suite[F, Res, *]]
191 | {
192 | def flatMap[A, B](fa: Suite[F, Res, A])(f: A => Suite[F, Res, B]): Suite[F, Res, B] =
193 | Suite.If(fa, f)
194 |
195 | def pure[A](a: A): Suite[F, Res, A] =
196 | Suite.Pure(a)
197 |
198 | def combineK[A](x: Suite[F, Res, A], y: Suite[F, Res, A]): Suite[F, Res, A] =
199 | Suite.Unless(x, y)
200 | }
201 |
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalacheck/ForAll.scala:
--------------------------------------------------------------------------------
1 | package org.scalacheck
2 |
3 | import cats.data.Kleisli
4 | import cats.effect.Sync
5 | import cats.implicits._
6 | import fs2.{Pull, Stream}
7 | import klk.PropertyTest
8 | import org.scalacheck.Gen.Parameters
9 | import org.scalacheck.util.Pretty
10 |
11 | object ForAll
12 | {
13 | def provedToTrue(r: Prop.Result) = r.status match {
14 | case Prop.Proof => r.copy(status = Prop.True)
15 | case _ => r
16 | }
17 |
18 | def addArg[A]
19 | (labels: String)
20 | (value: A, orig: A, shrinks: Int)
21 | (result: Prop.Result)
22 | (implicit pretty: A => Pretty)
23 | : Prop.Result =
24 | result.addArg(Prop.Arg(labels, value, shrinks, orig, pretty(value), pretty(orig)))
25 | }
26 |
27 | object ForAllNoShrink
28 | {
29 | def executeForArg[F[_]: Sync, A, P]
30 | (test: A => PropertyTest[F], prms: Parameters, genResult: Gen.R[A])
31 | (value: A)
32 | (implicit pp1: A => Pretty)
33 | : Kleisli[F, Parameters, Prop.Result] =
34 | Kleisli.local((_: Parameters) => prms)(test(value).test)
35 | .recoverWith { case e: Throwable => Kleisli.pure(Prop.Result(status = Prop.Exception(e))) }
36 | .map(ForAll.provedToTrue)
37 | .map(ForAll.addArg(genResult.labels.mkString(","))(value, value, 0))
38 |
39 | def apply[F[_]: Sync, A, P, Output]
40 | (test: A => PropertyTest[F])
41 | (implicit arbitrary: Arbitrary[A], pp1: A => Pretty)
42 | : PropertyTest[F] =
43 | PropertyTest {
44 | for {
45 | prms0 <- Kleisli.ask[F, Parameters]
46 | (prms, seed) <- Kleisli.liftF(Sync[F].delay(Prop.startSeed(prms0)))
47 | genResult <- Kleisli.liftF(Sync[F].delay(arbitrary.arbitrary.doApply(prms, seed)))
48 | a <- genResult.retrieve
49 | .map(executeForArg(test, Prop.slideSeed(prms0), genResult))
50 | .getOrElse(Kleisli.pure[F, Parameters, Prop.Result](Prop.undecided(prms)))
51 | } yield a
52 | }
53 | }
54 |
55 | object ForAllShrink
56 | {
57 | sealed trait ShrinkResult[A]
58 |
59 | object ShrinkResult
60 | {
61 | case class Success[A](result: Prop.Result)
62 | extends ShrinkResult[A]
63 |
64 | case class FirstFailure[A](value: A, result: Prop.Result)
65 | extends ShrinkResult[A]
66 | }
67 |
68 | def executeForArg[F[_]: Sync, A, P]
69 | (test: A => PropertyTest[F])
70 | (value: A)
71 | : Kleisli[F, Parameters, Prop.Result] =
72 | test(value).test
73 | .recoverWith { case e: Throwable => Kleisli.pure(Prop.Result(status = Prop.Exception(e))) }
74 | .map(ForAll.provedToTrue)
75 |
76 | def executeForArgWithSliddenSeed[F[_]: Sync, A, P]
77 | (test: A => PropertyTest[F], prms0: Parameters)
78 | (value: A)
79 | : Kleisli[F, Parameters, Prop.Result] =
80 | for {
81 | prms <- Kleisli.liftF(Sync[F].delay(Prop.slideSeed(prms0)))
82 | result <- Kleisli.local((_: Parameters) => prms)(executeForArg(test)(value))
83 | } yield result
84 |
85 | def firstFailureOrSuccess[F[_]: Sync, A]
86 | (test: A => PropertyTest[F], prms0: Parameters)
87 | (values: Stream[PropertyTest.K[F, *], A])
88 | : Pull[PropertyTest.K[F, *], ShrinkResult[A], Unit] = {
89 | def spin
90 | (firstFailure: Option[ShrinkResult[A]])
91 | (in: Stream[PropertyTest.K[F, *], A])
92 | : Pull[PropertyTest.K[F, *], ShrinkResult[A], Unit] =
93 | in.pull.uncons1.flatMap {
94 | case Some((value, tail)) =>
95 | for {
96 | result <- Pull.eval(executeForArgWithSliddenSeed(test, prms0)(value))
97 | _ <- {
98 | if (result.failure) spin(firstFailure.orElse(Some(ShrinkResult.FirstFailure(value, result))))(tail)
99 | else Pull.output1(ShrinkResult.Success[A](result)) >> Pull.done
100 | }
101 | } yield ()
102 | case None => firstFailure.traverse(Pull.output1) >> Pull.done
103 | }
104 | spin(None)(values)
105 | }
106 |
107 | def updateResult(r0: Prop.Result, r1: Prop.Result): Prop.Result =
108 | (r0.args,r1.args) match {
109 | case (a0 :: _, a1 :: as) =>
110 | r1.copy(
111 | args = a1.copy(
112 | origArg = a0.origArg,
113 | prettyOrigArg = a0.prettyOrigArg
114 | ) :: as
115 | )
116 | case _ => r1
117 | }
118 |
119 | def shrinker[F[_]: Sync, A]
120 | (test: A => PropertyTest[F], prms: Parameters, genResult: Gen.R[A], labels: String)
121 | (shrinks: Int, value: A, initialValue: A, previousResult: Prop.Result)
122 | (implicit arbitrary: Arbitrary[A], shrink: Shrink[A], pp1: A => Pretty)
123 | : Kleisli[F, Parameters, Prop.Result] =
124 | {
125 | val res = ForAll.addArg(labels)(value, initialValue, 0)(previousResult)
126 | Stream.unfold(Shrink.shrink[A](value).filter(genResult.sieve))(as => as.headOption.map(a => (a, as.drop(1))))
127 | .through(a => firstFailureOrSuccess[F, A](test, prms)(a).stream)
128 | .compile
129 | .last
130 | .flatMap {
131 | case Some(ShrinkResult.FirstFailure(failedValue, failedResult)) =>
132 | val newResult = updateResult(previousResult, failedResult)
133 | shrinker(test, prms, genResult, labels)(shrinks + 1, failedValue, initialValue, newResult)
134 | case Some(ShrinkResult.Success(result)) => PropertyTest.kleisli[F].pure(result)
135 | case None => PropertyTest.kleisli[F].pure(res)
136 | }
137 | }
138 |
139 | def executeAndShrink[F[_]: Sync, A, P]
140 | (test: A => PropertyTest[F], prms0: Parameters, genResult: Gen.R[A], labels: String)
141 | (value: A)
142 | (implicit arbitrary: Arbitrary[A], shrink: Shrink[A], pp1: A => Pretty)
143 | : Kleisli[F, Parameters, Prop.Result] =
144 | for {
145 | result <- executeForArgWithSliddenSeed(test, prms0)(value)
146 | finalResult <- {
147 | if (result.failure) shrinker(test, prms0, genResult, labels)(0, value, value, result)
148 | else PropertyTest.kleisli[F]
149 | .pure(ForAll.addArg(labels)(value, value, 0)(result))
150 | }
151 | } yield finalResult
152 |
153 | def apply[F[_]: Sync, A, P]
154 | (test: A => PropertyTest[F])
155 | (implicit arbitrary: Arbitrary[A], shrink: Shrink[A], pp1: A => Pretty)
156 | : PropertyTest[F] =
157 | PropertyTest {
158 | for {
159 | prms0 <- Kleisli.ask[F, Parameters]
160 | (prms, seed) <- Kleisli.liftF(Sync[F].delay(Prop.startSeed(prms0)))
161 | genResult <- Kleisli.liftF(Sync[F].delay(arbitrary.arbitrary.doApply(prms, seed)))
162 | a <- genResult.retrieve
163 | .map(executeAndShrink(test, prms0, genResult, genResult.labels.mkString(",")))
164 | .getOrElse(PropertyTest.kleisli[F].pure(Prop.undecided(prms)))
165 | } yield a
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/Classes.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import scala.concurrent.ExecutionContext
4 |
5 | import cats.{Applicative, Comonad, Functor}
6 | import cats.data.{EitherT, NonEmptyList}
7 | import cats.effect.{ConcurrentEffect, IO, Timer}
8 | import fs2.Stream
9 |
10 | import StringColor._
11 | import StringColors.color
12 |
13 | object Indent
14 | {
15 | def apply[T[_]: Functor](spaces: Int)(lines: T[String]): T[String] =
16 | lines.map(a => s"${" " * spaces}$a")
17 |
18 | }
19 |
20 | trait TestReporter[F[_]]
21 | {
22 | def result: String => Boolean => F[Unit]
23 | def failure: KlkResult.Details => F[Unit]
24 | def fatal: Throwable => F[Unit]
25 | }
26 |
27 | object TestReporter
28 | {
29 | def packageFrameFilter: List[String] =
30 | List(
31 | "cats.effect",
32 | "scala.runtime",
33 | "scala.concurrent",
34 | "java",
35 | )
36 |
37 | def sanitizeStacktrace(trace: List[StackTraceElement]): List[String] =
38 | trace
39 | .takeWhile(a => !a.getClassName.startsWith("klk.Compute"))
40 | .reverse
41 | .dropWhile(a => packageFrameFilter.exists(a.getClassName.startsWith))
42 | .reverse
43 | .map(_.toString)
44 |
45 | def formatFailure: KlkResult.Details => NonEmptyList[String] = {
46 | case KlkResult.Details.NoDetails =>
47 | NonEmptyList.one("test failed")
48 | case KlkResult.Details.Simple(info) =>
49 | info
50 | case KlkResult.Details.Complex(desc, target, actual) =>
51 | desc ::: Indent(2)(NonEmptyList.of(s"target: ${target.toString.green}", s"actual: ${actual.toString.magenta}"))
52 | }
53 |
54 | def formatFatal: Throwable => NonEmptyList[String] =
55 | error =>
56 | NonEmptyList(
57 | s"${"test threw".blue} ${error.toString.magenta}",
58 | Indent(2)(sanitizeStacktrace(error.getStackTrace.toList)),
59 | )
60 |
61 | def successSymbol: Boolean => String = {
62 | case false => "✘".red
63 | case true => "✔".green
64 | }
65 |
66 | def formatResult(desc: String)(success: Boolean): NonEmptyList[String] =
67 | NonEmptyList.one(s"${successSymbol(success)} $desc")
68 |
69 | def report[F[_]: Applicative, A]
70 | (reporter: TestReporter[F])
71 | (desc: String)
72 | (result: KlkResult[A])
73 | : F[Unit] =
74 | reporter.result(desc)(KlkResult.successful(result)) *>
75 | KlkResult.failures(result).traverse_(reporter.failure)
76 |
77 | def noop[F[_]: Applicative]: TestReporter[F] =
78 | NoopTestReporter()
79 | }
80 |
81 | case class NoopTestReporter[F[_]: Applicative]()
82 | extends TestReporter[F]
83 | {
84 | def result: String => Boolean => F[Unit] =
85 | _ => _ => ().pure[F]
86 |
87 | def failure: KlkResult.Details => F[Unit] =
88 | _ => ().pure[F]
89 |
90 | def fatal: Throwable => F[Unit] =
91 | _ => ().pure[F]
92 | }
93 |
94 | trait Compute[F[_]]
95 | {
96 | def run[A](thunk: F[A]): A
97 | }
98 |
99 | object Compute
100 | {
101 | def apply[F[_]](implicit instance: Compute[F]): Compute[F] =
102 | instance
103 |
104 | implicit def Compute_IO: Compute[IO] =
105 | new Compute[IO] {
106 | def run[A](thunk: IO[A]): A =
107 | thunk
108 | .unsafeRunSync
109 | }
110 |
111 | implicit def Compute_Comonad[F[_]: Comonad]: Compute[F] =
112 | new Compute[F] {
113 | def run[A](thunk: F[A]): A =
114 | thunk.extract
115 | }
116 | }
117 |
118 | trait ConsConcurrent[F[_]]
119 | {
120 | def apply(ec: ExecutionContext): ConcurrentEffect[F]
121 | }
122 |
123 | object ConsConcurrent
124 | {
125 | implicit def io: ConsConcurrent[IO] =
126 | new ConsConcurrent[IO] {
127 | def apply(ec: ExecutionContext): ConcurrentEffect[IO] =
128 | IO.ioConcurrentEffect(IO.contextShift(ec))
129 | }
130 | }
131 |
132 | trait ConsTimer[F[_]]
133 | {
134 | def apply(ec: ExecutionContext): Timer[F]
135 | }
136 |
137 | object ConsTimer
138 | {
139 | implicit def io: ConsTimer[IO] =
140 | new ConsTimer[IO] {
141 | def apply(ec: ExecutionContext): Timer[IO] =
142 | IO.timer(ec)
143 | }
144 | }
145 |
146 | trait TestResult[Output]
147 | {
148 | type Value
149 |
150 | def apply(output: Output): KlkResult[Value]
151 | }
152 |
153 | object TestResult
154 | {
155 | type Aux[Output, V] = TestResult[Output] { type Value = V }
156 |
157 | implicit def TestResult_KlkResult[A]: TestResult.Aux[KlkResult[A], A] =
158 | new TestResult[KlkResult[A]] {
159 | type Value = A
160 | def apply(output: KlkResult[A]): KlkResult[A] =
161 | output
162 | }
163 |
164 | implicit def TestResult_Boolean: TestResult.Aux[Boolean, Unit] =
165 | new TestResult[Boolean] {
166 | type Value = Unit
167 | def apply(output: Boolean): KlkResult[Unit] =
168 | KlkResult(output)(KlkResult.Details.NoDetails)
169 | }
170 |
171 | implicit def TestResult_Either[A, B]
172 | (implicit inner: TestResult.Aux[B, Unit])
173 | : TestResult.Aux[Either[A, B], Unit] =
174 | new TestResult[Either[A, B]] {
175 | type Value = Unit
176 | def apply(output: Either[A, B]): KlkResult[Value] =
177 | output
178 | .map(inner(_))
179 | .valueOr(a => KlkResult.simpleFailure(NonEmptyList.one(a.toString)))
180 | }
181 |
182 | implicit def TestResult_Option[A]
183 | (implicit inner: TestResult.Aux[A, Unit])
184 | : TestResult.Aux[Option[A], Unit] =
185 | new TestResult[Option[A]] {
186 | type Value = Unit
187 | def apply(output: Option[A]): KlkResult[Value] =
188 | output
189 | .map(inner(_))
190 | .getOrElse(KlkResult.simpleFailure(NonEmptyList.one("test returned None")))
191 | }
192 | }
193 |
194 | trait Compile[F[_], G[_], A]
195 | {
196 | type Value
197 |
198 | def apply(fa: F[A]): G[KlkResult[Value]]
199 | }
200 |
201 | object Compile
202 | {
203 | type Aux[F[_], G[_], A, V] = Compile[F, G, A] { type Value = V }
204 |
205 | implicit def Compile_F_F[F[_]: Functor, A, V]
206 | (implicit result: TestResult.Aux[A, V])
207 | : Compile.Aux[F, F, A, V] =
208 | new Compile[F, F, A] {
209 | type Value = V
210 |
211 | def apply(fa: F[A]): F[KlkResult[Value]] =
212 | fa.map(result(_))
213 | }
214 |
215 | implicit def Compile_EitherT_F[F[_]: Functor, G[_], E, A, V]
216 | (implicit inner: Compile.Aux[F, G, Either[E, A], V])
217 | : Compile.Aux[EitherT[F, E, ?], G, A, V] =
218 | new Compile[EitherT[F, E, ?], G, A] {
219 | type Value = V
220 | def apply(fa: EitherT[F, E, A]): G[KlkResult[Value]] =
221 | inner(fa.value)
222 | }
223 |
224 | implicit def Compile_Stream[F[_], G[_], A, V]
225 | (implicit inner: Compile.Aux[F, G, Option[A], V], streamCompiler: Stream.Compiler[F, F])
226 | : Compile.Aux[Stream[F, *], G, A, V] =
227 | new Compile[Stream[F, *], G, A] {
228 | type Value = V
229 |
230 | def apply(fa: Stream[F, A]): G[KlkResult[Value]] =
231 | inner(fa.compile.last)
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/core/src/main/scala/klk/Property.scala:
--------------------------------------------------------------------------------
1 | package klk
2 |
3 | import java.util.concurrent.{ExecutorService, ThreadPoolExecutor}
4 |
5 | import cats.{Applicative, Functor}
6 | import cats.data.{Kleisli, NonEmptyList}
7 | import cats.effect.{Concurrent, Sync}
8 | import fs2.{Pull, Stream}
9 | import fs2.concurrent.SignallingRef
10 | import org.{scalacheck => sc}
11 | import org.scalacheck.{Arbitrary, ForAllNoShrink, ForAllShrink, Gen, Prop, Test => PropTest}
12 | import org.scalacheck.Test.{Parameters => TestParameters}
13 | import org.scalacheck.util.{FreqMap, Pretty}
14 |
15 | import StringColor.StringColorOps
16 | import StringColors.color
17 |
18 | case class ScalacheckParams(test: TestParameters, gen: Gen.Parameters, sizeStep: Int, maxDiscarded: Float)
19 |
20 | object ScalacheckParams
21 | {
22 | def cons(test: TestParameters, gen: Gen.Parameters): ScalacheckParams = {
23 | val iterations = math.ceil(test.minSuccessfulTests / test.workers.toDouble)
24 | val sizeStep = math.round((test.maxSize - test.minSize) / (iterations * test.workers)).toInt
25 | val maxDiscarded = test.maxDiscardRatio * test.minSuccessfulTests
26 | ScalacheckParams(test, gen, sizeStep, maxDiscarded)
27 | }
28 |
29 | def default: ScalacheckParams =
30 | cons(TestParameters.default, Gen.Parameters.default)
31 | }
32 |
33 | case class PropertyTestState(stats: PropertyTestState.Stats, result: PropTest.Status)
34 |
35 | object PropertyTestState
36 | {
37 | case class Stats(finished: Boolean, iterations: Int, discarded: Int)
38 |
39 | object Stats
40 | {
41 | def zero: Stats =
42 | Stats(false, 0, 0)
43 | }
44 |
45 | def updateStats(status: Prop.Status, maxDiscarded: Float): Stats => Stats = {
46 | case Stats(finished, iterations, discarded) =>
47 | val (newFinished, newDiscarded) =
48 | status match {
49 | case Prop.True =>
50 | (finished, discarded)
51 | case Prop.False =>
52 | (true, discarded)
53 | case Prop.Proof =>
54 | (true, discarded)
55 | case Prop.Undecided =>
56 | (discarded + 1 > maxDiscarded, discarded + 1)
57 | case Prop.Exception(_) =>
58 | (true, discarded)
59 | }
60 | Stats(newFinished, iterations + 1, newDiscarded)
61 | }
62 |
63 | def updateResult
64 | (params: ScalacheckParams, discarded: Int)
65 | (current: PropTest.Status)
66 | (propResult: Prop.Result)
67 | : Prop.Status => PropTest.Status = {
68 | case Prop.True =>
69 | current
70 | case Prop.False =>
71 | PropTest.Failed(propResult.args, propResult.labels)
72 | case Prop.Proof =>
73 | PropTest.Proved(propResult.args)
74 | case Prop.Undecided if discarded + 1 > params.maxDiscarded =>
75 | PropTest.Exhausted
76 | case Prop.Undecided =>
77 | current
78 | case Prop.Exception(e) =>
79 | PropTest.PropException(propResult.args, e, propResult.labels)
80 | }
81 |
82 | def update(params: ScalacheckParams)(propResult: Prop.Result): PropertyTestState => PropertyTestState = {
83 | case PropertyTestState(stats, result) =>
84 | PropertyTestState(
85 | updateStats(propResult.status, params.maxDiscarded)(stats),
86 | updateResult(params, stats.discarded)(result)(propResult)(propResult.status),
87 | )
88 | }
89 |
90 | def zero: PropertyTestState =
91 | PropertyTestState(Stats.zero, PropTest.Passed)
92 | }
93 |
94 | case class PropertyTestResult(success: Boolean, stats: PropertyTestState.Stats, result: PropTest.Result)
95 |
96 | object PropertyTestResult
97 | {
98 | def noInput: PropertyTestResult =
99 | PropertyTestResult(false, PropertyTestState.Stats.zero, PropTest.Result(PropTest.Exhausted, 0, 0, FreqMap.empty))
100 |
101 | def formatArg: Prop.Arg[_] => List[String] = {
102 | case Prop.Arg(_, _, _, _, arg, origArg) =>
103 | val argFormatted = arg(Pretty.defaultParams)
104 | val origArgFormatted = origArg(Pretty.defaultParams)
105 | if (argFormatted == origArgFormatted) List(argFormatted.magenta)
106 | else List(argFormatted.magenta, s"original: $origArgFormatted".blue)
107 | }
108 |
109 | def formatArgs(args: List[Prop.Arg[_]]): List[String] =
110 | Indent(2)(args.flatMap(formatArg).toSet.toList)
111 |
112 | def resultDetails: PropertyTestResult => KlkResult.Details = {
113 | case PropertyTestResult(_, PropertyTestState.Stats(_, iterations, discarded), result) =>
114 | val message: NonEmptyList[String] = result.status match {
115 | case PropTest.Exhausted => NonEmptyList.one(s"exhausted after $iterations iterations, discarding $discarded")
116 | case PropTest.Passed => NonEmptyList.one(s"passed after $iterations iterations")
117 | case PropTest.Proved(_) => NonEmptyList.one(s"proved after $iterations iterations")
118 | case PropTest.Failed(args, labels) =>
119 | NonEmptyList(
120 | s"failed after $iterations iterations for",
121 | formatArgs(args) ::: labels.toList,
122 | )
123 | case PropTest.PropException(args, e, labels) =>
124 | val exception = NonEmptyList(e.getMessage, e.getStackTrace.toList.map(_.toString))
125 | NonEmptyList("failed with exception", formatArgs(args)).concat(labels.toList) ::: exception
126 | }
127 | KlkResult.Details.Simple(message)
128 | }
129 |
130 | def success: PropTest.Status => Boolean = {
131 | case PropTest.Exhausted => false
132 | case PropTest.Failed(_, _) => false
133 | case PropTest.PropException(_, _, _) => false
134 | case PropTest.Proved(_) => true
135 | case PropTest.Passed => true
136 | }
137 |
138 | def klkResult(result: PropertyTestResult): KlkResult[Unit] =
139 | KlkResult(result.success)(PropertyTestResult.resultDetails(result))
140 |
141 | implicit def TestResult_PropertyTestResult: TestResult.Aux[PropertyTestResult, Unit] =
142 | new TestResult[PropertyTestResult] {
143 | type Value = Unit
144 | def apply(result: PropertyTestResult): KlkResult[Unit] =
145 | klkResult(result)
146 | }
147 | }
148 |
149 | case class PropertyTest[F[_]](test: Kleisli[F, Gen.Parameters, Prop.Result])
150 |
151 | object PropertyTest
152 | {
153 | def finish[F[_]]: PropertyTestState => Pull[F, PropertyTestResult, Unit] = {
154 | case PropertyTestState(stats @ PropertyTestState.Stats(_, iterations, discarded), status) =>
155 | val result = PropertyTestResult(
156 | PropertyTestResult.success(status),
157 | stats,
158 | PropTest.Result(status, iterations, discarded, FreqMap.empty),
159 | )
160 | Pull.output1(result) *> Pull.done
161 | }
162 |
163 | def aggregateResults[F[_]]
164 | (terminate: SignallingRef[F, Boolean])
165 | (params: ScalacheckParams)
166 | (state: PropertyTestState)
167 | (in: Stream[F, Prop.Result])
168 | : Pull[F, PropertyTestResult, Unit] =
169 | in.pull.uncons1.flatMap {
170 | case Some((propResult, rest)) =>
171 | val updated = PropertyTestState.update(params)(propResult)(state)
172 | if (updated.stats.finished) Pull.eval(terminate.set(true)) >> finish[F](updated)
173 | else aggregateResults(terminate)(params)(updated)(rest)
174 | case None =>
175 | finish[F](state)
176 | }
177 |
178 | def concurrent[F[_]: Concurrent]
179 | (terminate: SignallingRef[F, Boolean])
180 | (params: ScalacheckParams)
181 | (test: PropertyTest[F])
182 | : Stream[F, PropertyTestResult] =
183 | Stream.range(params.test.minSize, params.test.maxSize, params.sizeStep)
184 | .interruptWhen(terminate)
185 | .map(params.gen.withSize)
186 | .map(test.test.run)
187 | .map(Stream.eval)
188 | .parJoin(params.test.workers)
189 | .through(in => aggregateResults(terminate)(params)(PropertyTestState.zero)(in).stream)
190 |
191 | def stream[F[_]: Concurrent]
192 | (params: ScalacheckParams)
193 | (test: PropertyTest[F])
194 | : Stream[F, PropertyTestResult] =
195 | for {
196 | terminate <- Stream.eval(SignallingRef(false))
197 | result <- concurrent(terminate)(params)(test)
198 | } yield result
199 |
200 | def discardingPool[F[_]: Sync](threads: Int): F[ExecutorService] =
201 | Concurrency.fixedPoolWith(threads).flatMap {
202 | case es: ThreadPoolExecutor =>
203 | Sync[F].delay(es.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy)).as(es)
204 | case es =>
205 | Sync[F].pure(es)
206 | }
207 |
208 | def run[F[_]: Sync]
209 | (concurrent: ConsConcurrent[F])
210 | (params: ScalacheckParams)
211 | (test: PropertyTest[F])
212 | : F[PropertyTestResult] =
213 | Concurrency.ec[F](discardingPool[F](params.test.workers))
214 | .map(concurrent(_))
215 | .map(stream(params)(test)(_))
216 | .use(_.compile.last.map(_.getOrElse(PropertyTestResult.noInput)))
217 |
218 | type K[F[_], A] = Kleisli[F, Gen.Parameters, A]
219 |
220 | class PropertyTestKleisli[F[_]: Applicative]
221 | {
222 | def pure[A](a: A): K[F, A] =
223 | Kleisli.pure(a)
224 | }
225 |
226 | def kleisli[F[_]: Applicative]: PropertyTestKleisli[F] =
227 | new PropertyTestKleisli[F]
228 | }
229 |
230 | object PropStatus
231 | {
232 | def bool: Boolean => Prop.Status = {
233 | case true => Prop.True
234 | case false => Prop.False
235 | }
236 |
237 | def finished: Prop.Status => Boolean = {
238 | case Prop.Exception(_) => true
239 | case Prop.Proof | Prop.False => true
240 | case _ => false
241 | }
242 | }
243 |
244 | object PropResult
245 | {
246 | def bool(a: Boolean): Prop.Result =
247 | Prop.Result(PropStatus.bool(a))
248 |
249 | def finished(result: Prop.Result): Boolean =
250 | PropStatus.finished(result.status)
251 | }
252 |
253 | trait PropTrans[F[_], Trans, A]
254 | {
255 | def apply(input: A => PropertyTest[F]): PropertyTest[F]
256 | }
257 |
258 | object PropTrans
259 | {
260 | type Full
261 | type Shrink
262 |
263 | implicit def PropTrans_Full[F[_]: Sync, A: Arbitrary]
264 | (implicit pp1: A => Pretty)
265 | : PropTrans[F, Full, A] =
266 | new PropTrans[F, Full, A] {
267 | def apply(f: A => PropertyTest[F]): PropertyTest[F] =
268 | ForAllNoShrink(f)
269 | }
270 |
271 | implicit def PropTrans_Shrink[F[_]: Sync, A: sc.Shrink: Arbitrary]
272 | (implicit pp1: A => Pretty)
273 | : PropTrans[F, Shrink, A] =
274 | new PropTrans[F, Shrink, A] {
275 | def apply(f: A => PropertyTest[F]): PropertyTest[F] =
276 | ForAllShrink(f)
277 | }
278 | }
279 |
280 | trait PropThunk[Thunk, Trans]
281 | {
282 | type F[A]
283 |
284 | def apply(thunk: Thunk): PropertyTest[F]
285 | }
286 |
287 | object PropThunk
288 | {
289 | type Aux[Thunk, Trans, F0[_]] = PropThunk[Thunk, Trans] { type F[A] = F0[A] }
290 |
291 | implicit def PropThunk_Output[F0[_]: Functor, Param, Output, Trans]
292 | (implicit pv: Output => Prop, trans: PropTrans[F0, Trans, Param])
293 | : PropThunk.Aux[Param => F0[Output], Trans, F0] =
294 | new PropThunk[Param => F0[Output], Trans] {
295 | type F[A] = F0[A]
296 | def apply(f: Param => F[Output]): PropertyTest[F] =
297 | trans(p => PropertyTest(Kleisli(params => f(p).map(out => pv(out)(params)))))
298 | }
299 |
300 | implicit def PropThunk_f[F0[_], Thunk, P, Trans]
301 | (implicit next: PropThunk.Aux[Thunk, Trans, F0], trans: PropTrans[F0, Trans, P])
302 | : PropThunk.Aux[P => Thunk, Trans, F0] =
303 | new PropThunk[P => Thunk, Trans] {
304 | type F[A] = F0[A]
305 | def apply(f: P => Thunk): PropertyTest[F] =
306 | trans(p => next(f(p)))
307 | }
308 | }
309 |
310 | trait PropRun[Thunk, Trans]
311 | {
312 | type F[A]
313 | def apply(thunk: Thunk): PropertyTest[F]
314 | def sync: Sync[F]
315 | def pool: ConsConcurrent[F]
316 | }
317 |
318 | object PropRun
319 | {
320 | type Aux[Thunk, Trans, F0[_]] = PropRun[Thunk, Trans] { type F[A] = F0[A] }
321 |
322 | implicit def PropRun_Any[Thunk, Trans, F0[_]]
323 | (implicit propThunk: PropThunk.Aux[Thunk, Trans, F0], syncF: Sync[F0], poolF: ConsConcurrent[F0])
324 | : PropRun.Aux[Thunk, Trans, F0] =
325 | new PropRun[Thunk, Trans] {
326 | type F[A] = F0[A]
327 | def apply(thunk: Thunk): PropertyTest[F] = propThunk(thunk)
328 | def sync: Sync[F] = syncF
329 | def pool: ConsConcurrent[F] = poolF
330 | }
331 |
332 | // TODO parameterize params
333 | def apply[Thunk, Trans]
334 | (propRun: PropRun[Thunk, Trans])
335 | (thunk: Thunk)
336 | : propRun.F[PropertyTestResult] = {
337 | implicit def functor: Functor[propRun.F] = propRun.sync
338 | PropertyTest.run(propRun.pool)(ScalacheckParams.default)(propRun(thunk))(propRun.sync)
339 | }
340 | }
341 |
--------------------------------------------------------------------------------