diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt new file mode 100644 index 0000000000..af241fdea0 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +fun assert(lambdaRecorder: LambdaRecorder): LambdaRecorderAssertions { + return lambdaRecorder.assertions() +} + +class LambdaRecorderAssertions internal constructor( + private val parametersSequence: List>, +) { + fun isCalledOnce(): CalledOnceParametersAssertions { + return CalledOnceParametersAssertions( + assertions = isCalledExactly(1) + ) + } + + fun isNeverCalled() { + isCalledExactly(0) + } + + fun isCalledExactly(times: Int): ParametersAssertions { + if (parametersSequence.size != times) { + throw AssertionError("Expected to be called $times, but was called ${parametersSequence.size} times") + } + return ParametersAssertions(parametersSequence) + } +} + +class CalledOnceParametersAssertions internal constructor(private val assertions: ParametersAssertions) { + fun with(vararg matchers: ParameterMatcher) { + assertions.withSequence(matchers.toList()) + } + + fun withNoParameter() { + assertions.withNoParameter() + } +} + +class ParametersAssertions internal constructor( + private val parametersSequence: List> +) { + fun withSequence(vararg matchersSequence: List) { + if (parametersSequence.size != matchersSequence.size) { + throw AssertionError("Lambda was called ${parametersSequence.size} times, but only ${matchersSequence.size} assertions were provided") + } + parametersSequence.zip(matchersSequence).forEachIndexed { invocationIndex, (parameters, matchers) -> + if (parameters.size != matchers.size) { + throw AssertionError("Expected ${matchers.size} parameters, but got ${parameters.size} parameters during invocation #$invocationIndex") + } + parameters.zip(matchers).forEachIndexed { paramIndex, (param, matcher) -> + if (!matcher.match(param)) { + throw AssertionError( + "Parameter #$paramIndex does not match the expected value (actual=$param,expected=$matcher) during invocation #$invocationIndex" + ) + } + } + } + } + + fun withNoParameter() { + if (parametersSequence.any { it.isNotEmpty() }) { + throw AssertionError("Expected no parameters, but got some") + } + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt new file mode 100644 index 0000000000..b7beaaa5e9 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +/** + * A recorder that can be used to record the parameters of lambda invocation. + */ +abstract class LambdaRecorder internal constructor( + private val assertNoInvocation: Boolean, +) { + private val parametersSequence: MutableList> = mutableListOf() + + internal fun onInvoke(vararg params: Any?) { + if (assertNoInvocation) { + throw AssertionError("This lambda should never be called.") + } + parametersSequence.add(params.toList()) + } + + fun assertions(): LambdaRecorderAssertions { + return LambdaRecorderAssertions(parametersSequence = parametersSequence) + } +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: () -> R +): LambdaNoParamRecorder { + return LambdaNoParamRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T) -> R +): LambdaOneParamRecorder { + return LambdaOneParamRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2) -> R +): LambdaTwoParamsRecorder { + return LambdaTwoParamsRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3) -> R +): LambdaThreeParamsRecorder { + return LambdaThreeParamsRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3, T4) -> R +): LambdaFourParamsRecorder { + return LambdaFourParamsRecorder(ensureNeverCalled, block) +} + +class LambdaNoParamRecorder(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R { + override fun invoke(): R { + onInvoke() + return block() + } +} + +class LambdaOneParamRecorder(ensureNeverCalled: Boolean, val block: (T) -> R) : LambdaRecorder(ensureNeverCalled), (T) -> R { + override fun invoke(p: T): R { + onInvoke(p) + return block(p) + } +} + +class LambdaTwoParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2) -> R) : LambdaRecorder(ensureNeverCalled), (T1, T2) -> R { + override fun invoke(p1: T1, p2: T2): R { + onInvoke(p1, p2) + return block(p1, p2) + } +} + +class LambdaThreeParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2, T3) -> R) : LambdaRecorder( + ensureNeverCalled +), (T1, T2, T3) -> R { + override fun invoke(p1: T1, p2: T2, p3: T3): R { + onInvoke(p1, p2, p3) + return block(p1, p2, p3) + } +} + +class LambdaFourParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2, T3, T4) -> R) : LambdaRecorder( + ensureNeverCalled +), (T1, T2, T3, T4) -> R { + override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4): R { + onInvoke(p1, p2, p3, p4) + return block(p1, p2, p3, p4) + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt new file mode 100644 index 0000000000..dd509ed262 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +/** + * A matcher that can be used to match parameters in lambda calls. + * This is useful to assert that a lambda has been called with specific parameters. + */ +interface ParameterMatcher { + fun match(param: Any?): Boolean +} + +/** + * A matcher that matches a specific value. + * Can be used to assert that a lambda has been called with a specific value. + */ +fun value(expectedValue: T) = object : ParameterMatcher { + override fun match(param: Any?) = param == expectedValue + override fun toString(): String = "value($expectedValue)" +} + +/** + * A matcher that matches any value. + * Can be used when we don't care about the value of a parameter. + */ +fun any() = object : ParameterMatcher { + override fun match(param: Any?) = true + override fun toString(): String = "any()" +}