Skip to content

Commit

Permalink
Fix #1033, introduce laws
Browse files Browse the repository at this point in the history
  • Loading branch information
rossabaker committed Dec 27, 2017
1 parent 8064c76 commit 4733d6b
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 111 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ lazy val commonSettings = Seq(
libraryDependencies ++= Seq(
compilerPlugin("org.spire-math" %% "kind-projector" % "0.9.4"),
"org.scalatest" %%% "scalatest" % "3.0.4" % "test",
"org.scalacheck" %%% "scalacheck" % "1.13.5" % "test"
"org.scalacheck" %%% "scalacheck" % "1.13.5" % "test",
"org.typelevel" %%% "cats-laws" % "1.0.0" % "test"
),
scmInfo := Some(ScmInfo(url("https://github.com/functional-streams-for-scala/fs2"), "git@github.com:functional-streams-for-scala/fs2.git")),
homepage := Some(url("https://github.com/functional-streams-for-scala/fs2")),
Expand Down
28 changes: 10 additions & 18 deletions core/shared/src/main/scala/fs2/Chunk.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import scala.collection.immutable.VectorBuilder
import scala.reflect.ClassTag
import java.nio.{ByteBuffer => JByteBuffer}

import cats.{Applicative, Eval, Foldable, Monad, Traverse}
import cats.{Applicative, Eq, Eval, Foldable, Monad, Traverse}
import cats.instances.all._

/**
Expand Down Expand Up @@ -69,7 +69,12 @@ abstract class Chunk[+O] {
def last: Option[O] = if (isEmpty) None else Some(apply(size - 1))

/** Creates a new chunk by applying `f` to each element in this chunk. */
def map[O2](f: O => O2): Chunk[O2]
def map[O2](f: O => O2): Chunk[O2] = {
val b = new VectorBuilder[O2]
b.sizeHint(size)
for (i <- 0 until size) b += f(apply(i))
Chunk.indexedSeq(b.result)
}

/** Splits this chunk in to two chunks at the specified index. */
def splitAt(n: Int): (Chunk[O], Chunk[O]) = {
Expand Down Expand Up @@ -343,7 +348,6 @@ object Chunk {
def apply(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[O], Chunk[O]) =
Boxed(values, offset, n) -> Boxed(values, offset + n, length - n)
override def map[O2](f: O => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: O: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Boxed { def apply[O](values: Array[O]): Boxed[O] = Boxed(values, 0, values.length) }
Expand All @@ -361,7 +365,6 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Boolean], Chunk[Boolean]) =
Booleans(values, offset, n) -> Booleans(values, offset + n, length - n)
override def map[O2](f: Boolean => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Boolean: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Booleans { def apply(values: Array[Boolean]): Booleans = Booleans(values, 0, values.length) }
Expand All @@ -379,7 +382,6 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Byte], Chunk[Byte]) =
Bytes(values, offset, n) -> Bytes(values, offset + n, length - n)
override def map[O2](f: Byte => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Byte: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Bytes { def apply(values: Array[Byte]): Bytes = Bytes(values, 0, values.length) }
Expand All @@ -396,14 +398,6 @@ object Chunk {
second.position(n + offset)
(ByteBuffer(first), ByteBuffer(second))
}
override def map[O2](f: Byte => O2): Chunk[O2] = {
val b = new VectorBuilder[O2]
b.sizeHint(size)
for (i <- offset until size + offset) {
b += f(buf.get(i))
}
indexedSeq(b.result)
}
override def toArray[O2 >: Byte: ClassTag]: Array[O2] = {
val bs = new Array[Byte](size)
val b = buf.duplicate
Expand All @@ -427,7 +421,6 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Short], Chunk[Short]) =
Shorts(values, offset, n) -> Shorts(values, offset + n, length - n)
override def map[O2](f: Short => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Short: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Shorts { def apply(values: Array[Short]): Shorts = Shorts(values, 0, values.length) }
Expand All @@ -445,7 +438,6 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Int], Chunk[Int]) =
Ints(values, offset, n) -> Ints(values, offset + n, length - n)
override def map[O2](f: Int => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Int: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Ints { def apply(values: Array[Int]): Ints = Ints(values, 0, values.length) }
Expand All @@ -463,7 +455,6 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Long], Chunk[Long]) =
Longs(values, offset, n) -> Longs(values, offset + n, length - n)
override def map[O2](f: Long => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Long: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Longs { def apply(values: Array[Long]): Longs = Longs(values, 0, values.length) }
Expand All @@ -481,7 +472,6 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Float], Chunk[Float]) =
Floats(values, offset, n) -> Floats(values, offset + n, length - n)
override def map[O2](f: Float => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Float: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Floats { def apply(values: Array[Float]): Floats = Floats(values, 0, values.length) }
Expand All @@ -499,11 +489,12 @@ object Chunk {
def at(i: Int) = values(offset + i)
protected def splitAtChunk_(n: Int): (Chunk[Double], Chunk[Double]) =
Doubles(values, offset, n) -> Doubles(values, offset + n, length - n)
override def map[O2](f: Double => O2): Chunk[O2] = seq(values.map(f))
override def toArray[O2 >: Double: ClassTag]: Array[O2] = values.slice(offset, offset + length).asInstanceOf[Array[O2]]
}
object Doubles { def apply(values: Array[Double]): Doubles = Doubles(values, 0, values.length) }

implicit def fs2EqForChunk[A: Eq]: Eq[Chunk[A]] = Eq.by(_.toVector)

implicit val instance: Traverse[Chunk] with Monad[Chunk] = new Traverse[Chunk] with Monad[Chunk] {
def foldLeft[A, B](fa: Chunk[A], b: B)(f: (B, A) => B): B = fa.foldLeft(b)(f)
def foldRight[A, B](fa: Chunk[A], b: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
Expand All @@ -513,6 +504,7 @@ object Chunk {
def traverse[F[_], A, B](fa: Chunk[A])(f: A => F[B])(implicit G: Applicative[F]): F[Chunk[B]] =
G.map(Traverse[Vector].traverse(fa.toVector)(f))(Chunk.vector)
def pure[A](a: A): Chunk[A] = Chunk.singleton(a)
override def map[A, B](fa: Chunk[A])(f: A => B): Chunk[B] = fa.map(f)
def flatMap[A,B](fa: Chunk[A])(f: A => Chunk[B]): Chunk[B] =
fa.toSegment.flatMap(f.andThen(_.toSegment)).force.toChunk
def tailRecM[A,B](a: A)(f: A => Chunk[Either[A,B]]): Chunk[B] =
Expand Down
194 changes: 104 additions & 90 deletions core/shared/src/test/scala/fs2/ChunkSpec.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package fs2

import cats.Eq
import cats.kernel.CommutativeMonoid
import cats.kernel.laws.discipline.EqTests
import cats.laws.discipline.{ MonadTests, TraverseTests }
import cats.implicits._
import java.nio.ByteBuffer
import org.scalacheck.{Gen, Arbitrary}
import org.scalacheck.{ Arbitrary, Cogen, Gen }
import scala.reflect.ClassTag

import ChunkProps._

class ChunkSpec extends Fs2Spec {

"Chunk" - {

"chunk-formation (1)" in {
Chunk.empty.toList shouldBe List()
Chunk.singleton(23).toList shouldBe List(23)
Expand All @@ -19,101 +24,110 @@ class ChunkSpec extends Fs2Spec {
Chunk.seq(c).toList shouldBe c.toList
Chunk.indexedSeq(c).toVector shouldBe c
Chunk.indexedSeq(c).toList shouldBe c.toList
// Chunk.seq(c).iterator.toList shouldBe c.iterator.toList
// Chunk.indexedSeq(c).iterator.toList shouldBe c.iterator.toList
}
}

implicit val arbBooleanChunk: Arbitrary[Chunk[Boolean]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Boolean](n, Arbitrary.arbBool.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.Booleans(values, offset, sz)
}

implicit val arbBooleanChunk: Arbitrary[Chunk[Boolean]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Boolean](n, Arbitrary.arbBool.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.booleans(values, offset, sz)
}
implicit val arbByteChunk: Arbitrary[Chunk[Byte]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Byte](n, Arbitrary.arbByte.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.bytes(values, offset, sz)
}

implicit val arbByteChunk: Arbitrary[Chunk[Byte]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Byte](n, Arbitrary.arbByte.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.bytes(values, offset, sz)
}
val arbByteBufferChunk: Arbitrary[Chunk[Byte]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Byte](n, Arbitrary.arbByte.arbitrary)
pos <- Gen.choose(0, n)
lim <- Gen.choose(pos, n)
direct <- Arbitrary.arbBool.arbitrary
bb = if (direct) ByteBuffer.allocateDirect(n).put(values) else ByteBuffer.wrap(values)
_ = bb.position(pos).limit(lim)
} yield Chunk.byteBuffer(bb)
}

implicit val arbByteBufferChunk: Arbitrary[Chunk.ByteBuffer] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Byte](n, Arbitrary.arbByte.arbitrary)
pos <- Gen.choose(0, n)
lim <- Gen.choose(pos, n)
direct <- Arbitrary.arbBool.arbitrary
bb = if (direct) ByteBuffer.allocateDirect(n).put(values) else ByteBuffer.wrap(values)
_ = bb.position(pos).limit(lim)
} yield Chunk.ByteBuffer(bb)
}
implicit val arbShortChunk: Arbitrary[Chunk[Short]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Short](n, Arbitrary.arbShort.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.shorts(values, offset, sz)
}

implicit val arbDoubleChunk: Arbitrary[Chunk[Double]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Double](n, Arbitrary.arbDouble.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.doubles(values, offset, sz)
}
implicit val arbIntChunk: Arbitrary[Chunk[Int]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Int](n, Arbitrary.arbInt.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.ints(values, offset, sz)
}

implicit val arbLongChunk: Arbitrary[Chunk[Long]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Long](n, Arbitrary.arbLong.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.longs(values, offset, sz)
}
implicit val arbLongChunk: Arbitrary[Chunk[Long]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Long](n, Arbitrary.arbLong.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.longs(values, offset, sz)
}

implicit val arbFloatChunk: Arbitrary[Chunk[Float]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Float](n, Arbitrary.arbFloat.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.floats(values, offset, sz)
}

"size.boolean" in propSize[Boolean, Chunk[Boolean]]
"size.byte" in propSize[Byte, Chunk[Byte]]
"size.byteBuffer" in propSize[Byte, Chunk.ByteBuffer]
"size.double" in propSize[Double, Chunk[Double]]
"size.long" in propSize[Long, Chunk[Long]]
"size.unspecialized" in propSize[Int, Chunk[Int]]

"take.boolean" in propTake[Boolean, Chunk[Boolean]]
"take.byte" in propTake[Byte, Chunk[Byte]]
"take.byteBuffer" in propTake[Byte, Chunk.ByteBuffer]
"take.double" in propTake[Double, Chunk[Double]]
"take.long" in propTake[Long, Chunk[Long]]
"take.unspecialized" in propTake[Int, Chunk[Int]]

"drop.boolean" in propDrop[Boolean, Chunk[Boolean]]
"drop.byte" in propDrop[Byte, Chunk[Byte]]
"drop.byteBuffer" in propDrop[Byte, Chunk.ByteBuffer]
"drop.double" in propDrop[Double, Chunk[Double]]
"drop.long" in propDrop[Long, Chunk[Long]]
"drop.unspecialized" in propDrop[Int, Chunk[Int]]

"isempty.boolean" in propIsEmpty[Boolean, Chunk[Boolean]]
"isempty.byte" in propIsEmpty[Byte, Chunk[Byte]]
"isempty.byteBuffer" in propIsEmpty[Byte, Chunk.ByteBuffer]
"isempty.double" in propIsEmpty[Double, Chunk[Double]]
"isempty.long" in propIsEmpty[Long, Chunk[Long]]
"isempty.unspecialized" in propIsEmpty[Int, Chunk[Int]]

"toarray.boolean" in propToArray[Boolean, Chunk[Boolean]]
"toarray.byte" in propToArray[Byte, Chunk[Byte]]
"toarray.byteBuffer" in propToArray[Byte, Chunk.ByteBuffer]
"toarray.double" in propToArray[Double, Chunk[Double]]
"toarray.long" in propToArray[Long, Chunk[Long]]
"toarray.unspecialized" in propToArray[Int, Chunk[Int]]

"tobytebuffer.byte" in propToByteBuffer[Chunk[Byte]]
"tobytebuffer.byteBuffer" in propToByteBuffer[Chunk.ByteBuffer]

// "map.boolean => byte" in forAll { c: Chunk[Boolean] =>
// (c map { b => if (b) 0.toByte else 1.toByte }).toArray shouldBe (c.toArray map { b => if (b) 0.toByte else 1.toByte })
// }

// "map.long => long" in forAll { c: Chunk[Long] => (c map (1 +)).toArray shouldBe (c.toArray map (1 +)) }
implicit val arbDoubleChunk: Arbitrary[Chunk[Double]] = Arbitrary {
for {
n <- Gen.choose(0, 100)
values <- Gen.containerOfN[Array, Double](n, Arbitrary.arbDouble.arbitrary)
offset <- Gen.choose(0, n)
sz <- Gen.choose(0, n - offset)
} yield Chunk.doubles(values, offset, sz)
}

def testChunk[A: Arbitrary: ClassTag: Cogen: CommutativeMonoid: Eq](name: String, of: String, testTraverse: Boolean = true)(implicit C: Arbitrary[Chunk[A]]): Unit = {
s"$name" - {
"size" in propSize[A, Chunk[A]]
"take" in propTake[A, Chunk[A]]
"drop" in propDrop[A, Chunk[A]]
"isempty" in propIsEmpty[A, Chunk[A]]
"toarray" in propToArray[A, Chunk[A]]

if (implicitly[ClassTag[A]] == ClassTag.Byte)
"tobytebuffer.byte" in propToByteBuffer[Chunk[Byte]]

checkAll(s"Eq[Chunk[$of]]", EqTests[Chunk[A]].eqv)
checkAll(s"Monad[Chunk]", MonadTests[Chunk].monad[A, A, A])

if (testTraverse)
checkAll(s"Traverse[Chunk]", TraverseTests[Chunk].traverse[A, A, A, A, Option, Option])
}
}

testChunk[Byte]("Bytes", "Byte")
testChunk[Short]("Shorts", "Short")
testChunk[Int]("Ints", "Int")
testChunk[Long]("Longs", "Long")
// Don't test traverse on Double or Float. They have naughty monoids.
testChunk[Double]("Doubles", "Double", false)
testChunk[Float]("Floats", "Float", false)
testChunk[Set[Boolean]]("Unspecialized", "Set[Boolean]")
testChunk[Byte]("ByteBuffer", "Byte")(implicitly, implicitly, implicitly, implicitly, implicitly, arbByteBufferChunk)
}
9 changes: 7 additions & 2 deletions core/shared/src/test/scala/fs2/Fs2Spec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ package fs2

import scala.concurrent.ExecutionContext

import org.typelevel.discipline.Laws
import org.scalatest.{ Args, AsyncFreeSpec, FreeSpec, Matchers, Status, Suite }
import org.scalatest.concurrent.{ AsyncTimeLimitedTests, TimeLimitedTests }
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalatest.prop.{ Checkers, GeneratorDrivenPropertyChecks }
import org.scalatest.time.Span

abstract class Fs2Spec extends FreeSpec with Fs2SpecLike with TimeLimitedTests {
abstract class Fs2Spec extends FreeSpec with Fs2SpecLike with TimeLimitedTests with Checkers {
val timeLimit: Span = timeout

def checkAll(name: String, ruleSet: Laws#RuleSet): Unit =
for ((id, prop) ruleSet.all.properties)
s"${name}.${id}" in check(prop)
}

abstract class AsyncFs2Spec extends AsyncFreeSpec with Fs2SpecLike with AsyncTimeLimitedTests {
Expand Down
4 changes: 4 additions & 0 deletions core/shared/src/test/scala/fs2/TestUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.scalacheck.{Arbitrary, Cogen, Gen, Shrink}

import scala.concurrent.Future
import scala.concurrent.duration._
import scala.reflect.ClassTag

import cats.effect.IO
import cats.implicits._
Expand Down Expand Up @@ -38,6 +39,9 @@ trait TestUtil extends TestUtilPlatform {
)
)

implicit def cogenChunk[A: Cogen: ClassTag]: Cogen[Chunk[A]] =
Cogen[Array[A]].contramap(_.toArray)

/** Newtype for generating test cases. Use the `tag` for labeling properties. */
case class PureStream[+A](tag: String, get: Stream[Pure,A])
implicit def arbPureStream[A:Arbitrary] = Arbitrary(PureStream.gen[A])
Expand Down

0 comments on commit 4733d6b

Please sign in to comment.