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

Assertions on Raise<E>.() -> A dont work on suspending functions #56

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<junit-jupiter.version>5.10.3</junit-jupiter.version>
<dokka-maven-plugin.version>1.9.0</dokka-maven-plugin.version>
<maven-surefire-plugin.version>3.3.1</maven-surefire-plugin.version>
<kotlinx-coroutines-test.version>1.9.0-RC.2</kotlinx-coroutines-test.version>
</properties>

<url>https://github.com/rcardin/assertj-arrow-core</url>
Expand Down Expand Up @@ -84,6 +85,13 @@
<version>${kotlin-stdlib.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-test</artifactId>
<version>${kotlinx-coroutines-test.version}</version>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
120 changes: 77 additions & 43 deletions src/main/kotlin/in/rcard/assertj/arrowcore/AbstractRaiseAssert.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package `in`.rcard.assertj.arrowcore

import arrow.core.raise.Raise
import arrow.core.raise.fold
import `in`.rcard.assertj.arrowcore.errors.RaiseShouldFailButSucceeds.Companion.shouldFailButSucceedsWith
import `in`.rcard.assertj.arrowcore.errors.RaiseShouldFailButSucceeds.Companion.shouldFailWithButSucceedsWith
import `in`.rcard.assertj.arrowcore.errors.RaiseShouldFailWith.Companion.shouldFailWith
Expand All @@ -25,84 +24,119 @@ abstract class AbstractRaiseAssert<
ERROR : Any,
VALUE : Any,
> internal constructor(
lambda: Raise<ERROR>.() -> VALUE,
raiseResult: RaiseResult<ERROR, VALUE>,
) : AbstractAssert<
SELF,
Raise<ERROR>.() -> VALUE,
>(lambda, AbstractRaiseAssert::class.java) {
RaiseResult<ERROR, VALUE>,
>(raiseResult, AbstractRaiseAssert::class.java) {
private val comparisonStrategy: ComparisonStrategy = StandardComparisonStrategy.instance()

/**
* Verifies that the function in the [Raise] context succeeds with the given value.
* @param expectedValue the expected value returned by the function.
*/
fun succeedsWith(expectedValue: VALUE) {
fold(
block = actual,
recover = { actualError: ERROR ->
fun succeedsWith(expectedValue: VALUE) =
when (actual) {
is RaiseResult.Failure<ERROR> -> {
throwAssertionError(
shouldSucceedWithButFailed(
expectedValue,
actualError,
),
shouldSucceedWithButFailed(expectedValue, (actual as RaiseResult.Failure<ERROR>).error),
)
},
transform = { actualValue ->
}

is RaiseResult.FailureWithException -> {
throw (actual as RaiseResult.FailureWithException).exception
}

is RaiseResult.Success<VALUE> -> {
val actualValue = (actual as RaiseResult.Success<VALUE>).value
if (!comparisonStrategy.areEqual(actualValue, expectedValue)) {
throwAssertionError(shouldSucceedWith(expectedValue, actualValue))
} else {
// Nothing to do
}
},
)
}
}
}

/**
* Verifies that the function in the [Raise] context succeeded. No check on the value returned by the function is
* performed.
*
* @see succeedsWith
*/
fun succeeds() {
fold(
block = actual,
recover = { actualError: ERROR -> throwAssertionError(shouldSucceedButFailed(actualError)) },
transform = { _ -> },
)
}
fun succeeds() =
when (actual) {
is RaiseResult.Failure<ERROR> ->
throwAssertionError(
shouldSucceedButFailed((actual as RaiseResult.Failure<ERROR>).error),
)

is RaiseResult.FailureWithException -> {
throw (actual as RaiseResult.FailureWithException).exception
}

is RaiseResult.Success<VALUE> -> {
// Nothing to do
}
}

/**
* Verifies that the function in the [Raise] context fails with the given error.
* @param expectedError the expected error raised by the function.
*/
fun raises(expectedError: ERROR) {
fold(
block = actual,
recover = { actualError ->
fun raises(expectedError: ERROR) =
when (actual) {
is RaiseResult.Failure<ERROR> -> {
val actualError = (actual as RaiseResult.Failure<ERROR>).error
if (!comparisonStrategy.areEqual(actualError, expectedError)) {
throwAssertionError(shouldFailWith(expectedError, actualError))
} else {
// Nothing to do
}
},
transform = { actualValue ->
}

is RaiseResult.FailureWithException -> {
throw (actual as RaiseResult.FailureWithException).exception
}

is RaiseResult.Success<VALUE> -> {
throwAssertionError(
shouldFailWithButSucceedsWith(expectedError, actualValue),
shouldFailWithButSucceedsWith(expectedError, (actual as RaiseResult.Success<VALUE>).value),
)
},
)
}
}
}

/**
* Verifies that the function in the [Raise] context fails, no matter the type of the logical error.
*
* @see raises
*/
fun fails() {
fold(
block = actual,
recover = { _ -> },
transform = { actualValue ->
fun fails() =
when (actual) {
is RaiseResult.Failure<ERROR> -> {
// Nothing to do
}

is RaiseResult.FailureWithException -> {
throw (actual as RaiseResult.FailureWithException).exception
}

is RaiseResult.Success ->
throwAssertionError(
shouldFailButSucceedsWith(actualValue),
shouldFailButSucceedsWith((actual as RaiseResult.Success<VALUE>).value),
)
},
)
}
}
}

sealed interface RaiseResult<out ERROR : Any, out VALUE : Any> {
data class Success<out VALUE : Any>(
val value: VALUE,
) : RaiseResult<Nothing, VALUE>

data class Failure<out ERROR : Any>(
val error: ERROR,
) : RaiseResult<ERROR, Nothing>

data class FailureWithException(
val exception: Throwable,
) : RaiseResult<Nothing, Nothing>
}
39 changes: 26 additions & 13 deletions src/main/kotlin/in/rcard/assertj/arrowcore/RaiseAssert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,42 @@ import kotlin.experimental.ExperimentalTypeInference
*
* @since 0.2.0
*/
class RaiseAssert<ERROR : Any, VALUE : Any> private constructor(lambda: Raise<ERROR>.() -> VALUE) :
AbstractRaiseAssert<RaiseAssert<ERROR, VALUE>, ERROR, VALUE>(lambda) {
class RaiseAssert<ERROR : Any, VALUE : Any>(
raiseResult: RaiseResult<ERROR, VALUE>,
) : AbstractRaiseAssert<RaiseAssert<ERROR, VALUE>, ERROR, VALUE>(raiseResult) {
companion object {
fun <ERROR : Any, VALUE : Any> assertThat(
@BuilderInference lambda: Raise<ERROR>.() -> VALUE
): RaiseAssert<ERROR, VALUE> =
RaiseAssert(lambda)
inline fun <ERROR : Any, VALUE : Any> assertThat(
@BuilderInference lambda: Raise<ERROR>.() -> VALUE,
): RaiseAssert<ERROR, VALUE> {
val raiseResult =
fold(
block = lambda,
catch = { throwable -> RaiseResult.FailureWithException(throwable) },
recover = { error -> RaiseResult.Failure(error) },
transform = { value -> RaiseResult.Success(value) },
)
return RaiseAssert(raiseResult)
}

/**
* Verifies that the function in the [Raise] context throws an exception.
* @param shouldRaiseThrowable the function to be executed in the [Raise] context.
* @return the [AbstractThrowableAssert] to be used to verify the exception.
*/
fun <ERROR : Any, VALUE : Any> assertThatThrownBy(
@BuilderInference shouldRaiseThrowable: Raise<ERROR>.() -> VALUE
inline fun <ERROR : Any, VALUE : Any> assertThatThrownBy(
@BuilderInference shouldRaiseThrowable: Raise<ERROR>.() -> VALUE,
): AbstractThrowableAssert<*, out Throwable> {
val throwable: Throwable? = fold(block = shouldRaiseThrowable,
recover = { null },
transform = { null },
catch = { exception -> exception })
val throwable: Throwable? =
fold(
block = shouldRaiseThrowable,
recover = { null },
transform = { null },
catch = { exception -> exception },
)

@Suppress("KotlinConstantConditions")
return throwable?.let { return Assertions.assertThat(throwable) } ?: throw Failures.instance()
return throwable?.let { return Assertions.assertThat(throwable) } ?: throw Failures
.instance()
.failure(Assertions.assertThat(throwable).writableAssertionInfo, shouldThrowAnException())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ private const val SHOULD_THROW_AN_EXCEPTION_MESSAGE = "%nExpecting code to throw
* @author Riccardo Cardin
* @since 0.2.0
*/
internal class RaiseShouldThrowAnException private constructor() :
BasicErrorMessageFactory(SHOULD_THROW_AN_EXCEPTION_MESSAGE) {

class RaiseShouldThrowAnException private constructor() : BasicErrorMessageFactory(SHOULD_THROW_AN_EXCEPTION_MESSAGE) {
companion object {
internal fun shouldThrowAnException(): RaiseShouldThrowAnException =
RaiseShouldThrowAnException()
fun shouldThrowAnException(): RaiseShouldThrowAnException = RaiseShouldThrowAnException()
}
}
}
12 changes: 6 additions & 6 deletions src/test/kotlin/in/rcard/assertj/arrowcore/Dummy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package `in`.rcard.assertj.arrowcore
import arrow.core.raise.Raise

internal object Dummy {
fun Raise<String>.aFunctionWithContext(input: Int): Int = input

context (Raise<String>)
fun aFunctionWithContext(input: Int): Int = input
fun Raise<String>.aFunctionThatRaisesAnError(): Int = raise("LOGICAL ERROR")

context (Raise<String>)
fun aFunctionThatRaisesAnError(): Int = raise("LOGICAL ERROR")
fun Raise<String>.aFunctionThatThrowsAnException(): Int = throw RuntimeException("AN EXCEPTION")

context (Raise<String>)
fun aFunctionThatThrowsAnException(): Int = throw RuntimeException("AN EXCEPTION")
suspend fun Raise<String>.aSuspendFunctionWithContext(input: Int): Int = input

suspend fun Raise<String>.aSuspendFunctionThatThrowsAnException(): Int = throw RuntimeException("AN EXCEPTION")
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
package `in`.rcard.assertj.arrowcore

import `in`.rcard.assertj.arrowcore.Dummy.aFunctionThatRaisesAnError
import `in`.rcard.assertj.arrowcore.Dummy.aFunctionThatThrowsAnException
import `in`.rcard.assertj.arrowcore.Dummy.aFunctionWithContext
import `in`.rcard.assertj.arrowcore.Dummy.aSuspendFunctionThatThrowsAnException
import `in`.rcard.assertj.arrowcore.RaiseAssert.Companion.assertThatThrownBy
import `in`.rcard.assertj.arrowcore.errors.RaiseShouldThrowAnException.Companion.shouldThrowAnException
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test

internal class RaiseAssert_assertThatThrownBy_Test {

@Test
internal fun `should succeed if the lambda throws an exception`() {
assertThatThrownBy { Dummy.aFunctionThatThrowsAnException() }
assertThatThrownBy { aFunctionThatThrowsAnException() }
.isInstanceOf(RuntimeException::class.java)
.hasMessage("AN EXCEPTION")
}

@Test
internal fun `should succeed if the suspending lambda throws an exception`() =
runTest {
assertThatThrownBy { aSuspendFunctionThatThrowsAnException() }
.isInstanceOf(RuntimeException::class.java)
.hasMessage("AN EXCEPTION")
}

@Test
internal fun `should fail if the lambda succeeds with a value`() {
Assertions.assertThatThrownBy {
assertThatThrownBy { Dummy.aFunctionWithContext(42) }
}.isInstanceOf(AssertionError::class.java)
Assertions
.assertThatThrownBy {
assertThatThrownBy { aFunctionWithContext(42) }
}.isInstanceOf(AssertionError::class.java)
.hasMessage(
shouldThrowAnException().create(),
)
}

@Test
internal fun `should fail if the lambda raises an error`() {
Assertions.assertThatThrownBy {
assertThatThrownBy { Dummy.aFunctionThatRaisesAnError() }
}.isInstanceOf(AssertionError::class.java)
Assertions
.assertThatThrownBy {
assertThatThrownBy { aFunctionThatRaisesAnError() }
}.isInstanceOf(AssertionError::class.java)
.hasMessage(
shouldThrowAnException().create(),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package `in`.rcard.assertj.arrowcore

import `in`.rcard.assertj.arrowcore.Dummy.aFunctionWithContext
import `in`.rcard.assertj.arrowcore.Dummy.aSuspendFunctionWithContext
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test

internal class RaiseAssert_assertThat_Test {
@Test
internal fun `should create an assertion instance when given lambda is not null`() {
val assertion = RaiseAssert.assertThat { Dummy.aFunctionWithContext(42) }
val assertion = RaiseAssert.assertThat { aFunctionWithContext(42) }
then(assertion).isNotNull.isInstanceOf(RaiseAssert::class.java)
}

@Test
internal fun `should create an assertion instance for suspending lambdas`() =
runTest {
val assertion = RaiseAssert.assertThat { aSuspendFunctionWithContext(42) }
then(assertion).isNotNull.isInstanceOf(RaiseAssert::class.java)
}
}
Loading