Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More Either tests #2398

Merged
merged 6 commits into from
May 12, 2021
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 151 additions & 11 deletions arrow-libs/core/arrow-core/src/test/kotlin/arrow/core/EitherTest.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package arrow.core

import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.computations.EitherEffect
import arrow.core.computations.RestrictedEitherEffect
import arrow.core.computations.either
import arrow.core.Either.Right
import arrow.core.Either.Left
import arrow.core.test.UnitSpec
import arrow.core.test.generators.any
import arrow.core.test.generators.either
Expand All @@ -18,6 +18,7 @@ import arrow.core.test.laws.FxLaws
import arrow.core.test.laws.MonoidLaws
import arrow.typeclasses.Monoid
import io.kotlintest.properties.Gen
import io.kotlintest.properties.PropertyContext
import io.kotlintest.properties.forAll
import io.kotlintest.shouldBe
import io.kotlintest.shouldThrow
Expand All @@ -38,6 +39,61 @@ class EitherTest : UnitSpec() {
}
)

"isLeft should return true if Left and false if Right" {
forAll { a: Int ->
Left(a).isLeft() && !Right(a).isLeft()
}
}

"isRight should return false if Left and true if Right" {
forAll { a: Int ->
!Left(a).isRight() && Right(a).isRight()
}
}

"fold should apply first op if Left and second op if Right" {
forAllSmallInt { a: Int, b: Int ->
val right: Either<Int, Int> = Right(a)
val left: Either<Int, Int> = Left(b)

right.fold({ it + 2 }, { it + 1 }) == a + 1 &&
left.fold({ it + 2 }, { it + 1 }) == b + 2
}
}

"foldLeft should return initial if Left and apply op if Right" {
forAllSmallInt { a: Int, b: Int, c: Int ->
Right(a).foldLeft(c, Int::plus) == c + a &&
Left(b).foldLeft(c, Int::plus) == c
}
}

"foldMap should return the empty of the inner type if Left and apply op if Right" {
forAllSmallInt { a: Int, b: Int ->
val left: Either<Int, Int> = Left(b)

Right(a).foldMap(Monoid.int()) { it + 1 } == a + 1 &&
left.foldMap(Monoid.int()) { it + 1 } == Monoid.int().empty()
}
}

"bifoldLeft should apply first op if Left and apply second op if Right" {
forAllSmallInt { a: Int, b: Int, c: Int ->
Right(a).bifoldLeft(c, Int::plus, Int::times) == a * c &&
Left(b).bifoldLeft(c, Int::plus, Int::times) == b + c
}
}

"bifoldMap should apply first op if Left and apply second op if Right" {
forAllSmallInt { a: Int, b: Int ->
val right: Either<Int, Int> = Right(a)
val left: Either<Int, Int> = Left(b)

right.bifoldMap(Monoid.int(), { it + 2 }, { it + 1 }) == a + 1 &&
left.bifoldMap(Monoid.int(), { it + 2 }, { it + 1 }) == b + 2
}
}

"fromNullable should lift value as a Right if it is not null" {
forAll { a: Int ->
Either.fromNullable(a) == Right(a)
Expand All @@ -49,9 +105,7 @@ class EitherTest : UnitSpec() {
}

"empty should return a Right of the empty of the inner type" {
forAll { _: String ->
Right(Monoid.string().empty()) == Monoid.either(Monoid.string(), Monoid.string()).empty()
}
Right(Monoid.string().empty()) shouldBe Monoid.either(Monoid.string(), Monoid.string()).empty()
}

"combine two rights should return a right of the combine of the inners" {
Expand Down Expand Up @@ -82,7 +136,7 @@ class EitherTest : UnitSpec() {

"orNull should return value" {
forAll { a: Int ->
Either.Right(a).orNull() == a
Right(a).orNull() == a
}
}

Expand All @@ -94,7 +148,7 @@ class EitherTest : UnitSpec() {
}

"filterOrElse should filter values" {
forAll(Gen.intSmall(), Gen.intSmall()) { a: Int, b: Int ->
forAllSmallInt { a: Int, b: Int ->
val left: Either<Int, Int> = Left(a)

Right(a).filterOrElse({ it > a - 1 }, { b }) == Right(a) &&
Expand All @@ -105,7 +159,7 @@ class EitherTest : UnitSpec() {
}

"filterOrOther should filter values" {
forAll(Gen.intSmall(), Gen.intSmall()) { a: Int, b: Int ->
forAllSmallInt { a: Int, b: Int ->
val left: Either<Int, Int> = Left(a)

Right(a).filterOrOther({ it > a - 1 }, { b + a }) == Right(a) &&
Expand All @@ -123,6 +177,17 @@ class EitherTest : UnitSpec() {
}
}

"exists should apply predicate to Right only" {
forAllSmallInt { a: Int ->
val left: Either<Int, Int> = Left(a)

Right(a).exists { it > a - 1 } &&
!Right(a).exists { it > a + 1 } &&
!left.exists { it > a - 1 } &&
!left.exists { it > a + 1 }
}
}

"rightIfNotNull should return Left if value is null or Right of value when not null" {
forAll { a: Int, b: Int ->
null.rightIfNotNull { b } == Left(b) &&
Expand All @@ -146,13 +211,15 @@ class EitherTest : UnitSpec() {

"orNull should convert" {
forAll { a: Int ->
val left: Either<Int, Int> = Left(a)

Right(a).orNull() == a &&
Left(a).orNull() == null
left.orNull() == null
}
}

"contains should check value" {
forAll(Gen.intSmall(), Gen.intSmall()) { a: Int, b: Int ->
forAllSmallInt { a: Int, b: Int ->
val rightContains = Right(a).contains(a)
// We need to check that a != b or this test will result in a false negative
val rightDoesntContains = if (a != b) !Right(a).contains(b) else true
Expand All @@ -162,14 +229,78 @@ class EitherTest : UnitSpec() {
}
}

"map should alter right instance only" {
forAllSmallInt { a: Int, b: Int ->
val right: Either<Int, Int> = Right(a)
val left: Either<Int, Int> = Left(b)

right.map { it + 1 } == Right(a + 1) && left.map { it + 1 } == left
}
}

"mapLeft should alter left instance only" {
forAll(Gen.intSmall(), Gen.intSmall()) { a: Int, b: Int ->
forAllSmallInt { a: Int, b: Int ->
val right: Either<Int, Int> = Right(a)
val left: Either<Int, Int> = Left(b)

right.mapLeft { it + 1 } == right && left.mapLeft { it + 1 } == Left(b + 1)
}
}

"bimap should alter left or right instance accordingly" {
forAllSmallInt { a: Int, b: Int ->
val right: Either<Int, Int> = Right(a)
val left: Either<Int, Int> = Left(b)

right.bimap({ it + 2 }, { it + 1 }) == Right(a + 1) &&
left.bimap({ it + 2 }, { it + 1 }) == Left(b + 2)
}
}

"replicate should return Right(empty list) when n <= 0" {
forAll(
Gen.oneOf(Gen.negativeIntegers(), Gen.constant(0)),
Gen.int()
) { n: Int, a: Int ->
val expected: Either<Int, List<Int>> = Right(emptyList())

Right(a).replicate(n) == expected &&
Left(a).replicate(n) == expected
}
}

"replicate should return Right(list of repeated value size n) when Right and n is positive" {
forAll(
Gen.intSmall().filter { it > 0 },
Gen.int()
) { n: Int, a: Int ->
Right(a).replicate(n) == Right(List(n) { a }) &&
Left(a).replicate(n) == Left(a)
}
}

"traverse should return list of Right when Right and empty list when Left" {
forAll(
Gen.int(),
Gen.int(),
Gen.int()
) { a: Int, b: Int, c: Int ->
Copy link
Contributor Author

@a-ivanov a-ivanov May 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gen.list is also possible, but I thought that showing list of Right is more explicit.

Right(a).traverse { emptyList<Int>() } == emptyList<Int>() &&
Right(a).traverse { listOf(b, c) } == listOf(Right(b), Right(c)) &&
Left(a).traverse { listOf(b, c) } == emptyList<Int>()
}
}

"flatMap should map right instance only" {
forAllSmallInt { a: Int, b: Int ->
val right: Either<Int, Int> = Right(a)
val left: Either<Int, Int> = Left(b)

right.flatMap { Right(it + 1) } == Right(a + 1) &&
left.flatMap { Right(it + 1) } == left
}
}

"conditionally should create right instance only if test is true" {
forAll { t: Boolean, i: Int, s: String ->
val expected = if (t) Right(i) else Left(s)
Expand Down Expand Up @@ -323,3 +454,12 @@ private suspend fun <A> throwException(
a: A
): Either<Throwable, Any> =
throw RuntimeException("An Exception is thrown while handling the result of the supplied function.")

private fun forAllSmallInt(fn: PropertyContext.(a: Int) -> Boolean) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we have some other utilities like these somewhere. Perhaps these functions should be moved there and made public or internal.

forAll(Gen.intSmall(), fn)

private fun forAllSmallInt(fn: PropertyContext.(a: Int, b: Int) -> Boolean) =
forAll(Gen.intSmall(), Gen.intSmall(), fn)

private fun forAllSmallInt(fn: PropertyContext.(a: Int, b: Int, c: Int) -> Boolean) =
forAll(Gen.intSmall(), Gen.intSmall(), Gen.intSmall(), fn)