Skip to content

Commit

Permalink
Merge pull request #548 from bwiercinski/issue528_effectful_discipline
Browse files Browse the repository at this point in the history
create DisciplineFSuite #528
  • Loading branch information
Baccata authored Jul 29, 2022
2 parents ccee77f + 88640c6 commit a318e98
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 68 deletions.
2 changes: 1 addition & 1 deletion modules/core/src-jvm/weaver/junit/WeaverRunner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class WeaverRunner(cls: Class[_], dummy: Boolean)
Reflection.loadRunnableSuite(cls.getName(), getClass().getClassLoader())
}

lazy val testDescriptions: Map[String, Description] = {
def testDescriptions: Map[String, Description] = {
suite.plan.map(name =>
name.name -> Description.createTestDescription(cls, name.name)).toMap
}
Expand Down
126 changes: 112 additions & 14 deletions modules/discipline/src/weaver/discipline/Discipline.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,115 @@ package discipline

import scala.util.control.NoStackTrace

import cats.data.Kleisli
import cats.effect.Resource
import cats.implicits._

import fs2.Stream
import org.scalacheck.Prop.Arg
import org.scalacheck.Test
import org.scalacheck.Test.{ Exhausted, Failed, Passed, PropException, Proved }
import org.scalacheck.Test._
import org.scalacheck.util.Pretty
import org.scalacheck.{ Prop, Test => ScalaCheckTest }
import org.typelevel.discipline.Laws

trait Discipline { self: FunSuiteAux =>
import Discipline._

import Expectations.Helpers._
import Discipline._
trait Discipline { self: FunSuiteAux =>

def checkAll(
name: TestName,
ruleSet: Laws#RuleSet,
parameters: Test.Parameters => Test.Parameters = identity): Unit =
parameters: Parameters => Parameters = identity): Unit =
ruleSet.all.properties.toList.foreach {
case (id, prop) =>
test(name.copy(s"${name.name}: $id")) {
Test.check(prop)(parameters).status match {
case Passed | Proved(_) => success
case Exhausted => failure("Property exhausted")(name.location)
case Failed(input, _) =>
failure(s"Property violated \n" + printArgs(input))(name.location)
case PropException(input, cause, _) =>
throw PropertyException(input, cause)
}
executeProp(prop, name.location, parameters)
}
}

}

trait DisciplineFSuite[F[_]] extends RunnableSuite[F] {

type Res
def sharedResource: Resource[F, Res]

/**
* Defines max parallelism within whole suite (maxSuiteParallelism = 1 means
* each checkAll will be run sequentially)
*/
def maxSuiteParallelism: Int = 10000

/**
* Defines max parallelism within single rule set (maxRuleSetParallelism = 1
* means each property of a law will be run sequentially)
*/
def maxRuleSetParallelism: Int = 10000

protected def registerTest(tests: Res => F[List[F[TestOutcome]]]): Unit =
testsSeq.synchronized {
if (isInitialized) throw initError()
testsSeq = testsSeq :+ tests
}

def checkAll(
name: TestName,
parameters: Parameters => Parameters = identity
): PartiallyAppliedCheckAll = new PartiallyAppliedCheckAll(name, parameters)

class PartiallyAppliedCheckAll(
name: TestName,
parameters: Parameters => Parameters) {
def apply(run: => F[Laws#RuleSet]): Unit = apply(_ => run)
def apply(run: Res => F[Laws#RuleSet]): Unit = {
registerTest(
Kleisli(run).map(_.all.properties.toList.map {
case (id, prop) =>
val propTestName = s"${name.name}: $id"
val runProp = effectCompat.effect.delay(
executeProp(prop, name.location, parameters)
)
foundProps.synchronized {
foundProps = foundProps :+ name.copy(propTestName)
}
Test(propTestName, runProp)
}).run
)
}

// this alias helps using pattern matching on `Res`
def usingRes(run: Res => F[Laws#RuleSet]): Unit = apply(run)

def pure(run: Res => Laws#RuleSet): Unit = apply(run.andThen(_.pure[F]))
}

override def spec(args: List[String]): Stream[F, TestOutcome] =
testsSeq.synchronized {
if (!isInitialized) isInitialized = true
val suiteParallelism = math.max(1, maxSuiteParallelism)
val ruleSetParallelism = math.max(1, maxRuleSetParallelism)
Stream.resource(sharedResource).flatMap { resource =>
Stream.emits(testsSeq).covary[F]
.parEvalMap(suiteParallelism)(_.apply(resource))
.map { ruleSet =>
Stream.emits(ruleSet).covary[F]
.parEvalMap(ruleSetParallelism)(identity)
}
.parJoin(suiteParallelism)
}
}

override def plan: List[TestName] =
foundProps.synchronized { foundProps.toList }

private[this] var foundProps = Seq.empty[TestName]

private[this] var testsSeq = Seq.empty[Res => F[List[F[TestOutcome]]]]

private[this] var isInitialized = false

private[this] def initError() = new AssertionError(
"Cannot define new tests after TestSuite was initialized")
}

object Discipline {
Expand All @@ -44,6 +125,23 @@ object Discipline {
"Property failed with an exception\n" + printArgs(input)
}

private[discipline] def executeProp(
prop: Prop,
location: SourceLocation,
parameters: Parameters => Parameters
): Expectations = {
import Expectations.Helpers._

ScalaCheckTest.check(prop)(parameters).status match {
case Passed | Proved(_) => success
case Exhausted => failure("Property exhausted")(location)
case Failed(input, _) =>
failure(s"Property violated \n" + printArgs(input))(location)
case PropException(input, cause, _) =>
throw PropertyException(input, cause)
}
}

private def printArgs(args: Seq[Arg[Any]]) =
args.zipWithIndex.map { case (arg, idx) =>
s"ARG $idx: " + arg.prettyArg(Pretty.defaultParams)
Expand Down
87 changes: 87 additions & 0 deletions modules/discipline/test/src/DisciplineFSuiteIntegrationTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package weaver.discipline

import cats.effect.{ IO, Resource }

import weaver.{ BaseIOSuite, SimpleIOSuite }

object DisciplineFSuiteIntegrationTest extends SimpleIOSuite {

test("Runs tests successfully") {
MetaSuccess.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.map(_.name).toSet == Set("Int: rickroll.rolls",
"Int: rickroll.ricks"),
outcomes.map(_.status.isFailed).forall(_ == false)
)
}
}

test("Reports failures correctly") {
MetaFailure.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.map(_.name).toSet == Set("Boolean: rickroll.rolls",
"Boolean: rickroll.ricks"),
outcomes.map(_.status.isFailed).count(_ == true) == 1,
outcomes.find(_.status.isFailed).exists { to =>
to.name == "Boolean: rickroll.ricks"
}
)
}
}

test("Captures exceptions correctly") {
import RickRoll._
MetaException.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.forall(_.cause.isDefined),
outcomes.flatMap(_.cause).collect {
case `oops` | `oops2` => true
case _ => false
}.size == 2
)
}
}

test("Shared resource fails to start") {
FailingResource.spec(Nil).compile.toList.attempt.map { outcomes =>
expect.all(
outcomes.isLeft,
outcomes.left.exists(_ == resourceStart)
)
}
}

object MetaSuccess extends DisciplineFSuite[IO] with BaseIOSuite {
override type Res = String
override def sharedResource: Resource[IO, String] =
Resource.pure("resource")

checkAll("Int").pure(_ => RickrollTests[Int].all)
}

object MetaFailure extends DisciplineFSuite[IO] with BaseIOSuite {
override type Res = String
override def sharedResource: Resource[IO, String] =
Resource.pure("resource")

checkAll("Boolean").pure(_ => RickrollTests[Boolean].all)
}

object MetaException extends DisciplineFSuite[IO] with BaseIOSuite {
override type Res = String
override def sharedResource: Resource[IO, String] =
Resource.pure("resource")

checkAll("String").pure(_ => RickrollTests[String].all)
}

object resourceStart extends Exception
object FailingResource extends DisciplineFSuite[IO] with BaseIOSuite {
override type Res = String
override def sharedResource: Resource[IO, String] =
Resource.eval(IO.raiseError(resourceStart))

checkAll("Int").pure(_ => RickrollTests[Int].all)
}

}
54 changes: 54 additions & 0 deletions modules/discipline/test/src/DisciplineIntegrationTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package weaver.discipline

import weaver.SimpleIOSuite

object DisciplineIntegrationTest extends SimpleIOSuite {

test("Runs tests successfully") {
MetaSuccess.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.map(_.name).toSet == Set("Int: rickroll.rolls",
"Int: rickroll.ricks"),
outcomes.map(_.status.isFailed).forall(_ == false)
)
}
}

test("Reports failures correctly") {
MetaFailure.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.map(_.name).toSet == Set("Boolean: rickroll.rolls",
"Boolean: rickroll.ricks"),
outcomes.map(_.status.isFailed).count(_ == true) == 1,
outcomes.find(_.status.isFailed).exists { to =>
to.name == "Boolean: rickroll.ricks"
}
)
}
}

test("Captures exceptions correctly") {
import RickRoll._
MetaException.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.forall(_.cause.isDefined),
outcomes.flatMap(_.cause).collect {
case `oops` | `oops2` => true
case _ => false
}.size == 2
)
}
}

object MetaSuccess extends weaver.FunSuite with Discipline {
checkAll("Int", RickrollTests[Int].all)
}

object MetaFailure extends weaver.FunSuite with Discipline {
checkAll("Boolean", RickrollTests[Boolean].all)
}

object MetaException extends weaver.FunSuite with Discipline {
checkAll("String", RickrollTests[String].all)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,62 +3,9 @@ package weaver.discipline
import cats.kernel.Eq
import cats.laws.discipline._

import weaver.SimpleIOSuite

import org.scalacheck.Arbitrary
import org.typelevel.discipline.Laws

object IntegrationTest extends SimpleIOSuite {

test("Runs tests successfully") {
MetaSuccess.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.map(_.name).toSet == Set("Int: rickroll.rolls",
"Int: rickroll.ricks"),
outcomes.map(_.status.isFailed).forall(_ == false)
)
}
}

test("Reports failures correctly") {
MetaFailure.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.map(_.name).toSet == Set("Boolean: rickroll.rolls",
"Boolean: rickroll.ricks"),
outcomes.map(_.status.isFailed).count(_ == true) == 1,
outcomes.find(_.status.isFailed).exists { to =>
to.name == "Boolean: rickroll.ricks"
}
)
}
}

test("Captures exceptions correctly") {
import RickRoll._
MetaException.spec(Nil).compile.toList.map { outcomes =>
expect.all(
outcomes.forall(_.cause.isDefined),
outcomes.flatMap(_.cause).collect {
case `oops` | `oops2` => true
case _ => false
}.size == 2
)
}
}

object MetaSuccess extends weaver.FunSuite with Discipline {
checkAll("Int", RickrollTests[Int].all)
}

object MetaFailure extends weaver.FunSuite with Discipline {
checkAll("Boolean", RickrollTests[Boolean].all)
}

object MetaException extends weaver.FunSuite with Discipline {
checkAll("String", RickrollTests[String].all)
}
}

trait RickRoll[A] {
def rick(a: A): A
def roll(a: A): Option[A]
Expand Down

0 comments on commit a318e98

Please sign in to comment.