├── 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 | --------------------------------------------------------------------------------