From 44125567f63f2c5a20ac20eca48e1d194cc0adda Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2024 10:31:49 +0100 Subject: [PATCH 1/2] Tests : create lambda recorder --- .../tests/testutils/lambda/Assertions.kt | 92 ++++++++++++++++++ .../tests/testutils/lambda/LambdaRecorder.kt | 97 +++++++++++++++++++ .../testutils/lambda/ParameterMatcher.kt | 41 ++++++++ 3 files changed, 230 insertions(+) create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt 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..4b08a160ba --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt @@ -0,0 +1,92 @@ +/* + * 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("Expected ${matchersSequence.size} parameters, but got ${parametersSequence.size} parameters") + } + parametersSequence.zip(matchersSequence).forEach { (parameters, matchers) -> + if (parameters.size != matchers.size) { + throw AssertionError("Expected ${matchers.size} parameters, but got ${parameters.size} parameters") + } + parameters.zip(matchers).forEachIndexed { j, (param, matcher) -> + if (!matcher.match(param)) { + throw AssertionError("Parameter $j does not match the expected value") + } + } + } + for (i in parametersSequence.indices) { + val params = parametersSequence[i] + val checker = matchersSequence[i] + if (params.size != checker.size) { + throw AssertionError("Expected ${checker.size} parameters, but got ${params.size} parameters") + } + for (j in params.indices) { + val param = params[j] + val check = checker[j] + if (!check.match(param)) { + throw AssertionError("Parameter $j does not match the expected value") + } + } + } + } + + 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..52c3dc63a8 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -0,0 +1,97 @@ +/* + * 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 parametersSequence: MutableList> = mutableListOf() + + internal fun onInvoke(vararg params: Any?) { + parametersSequence.add(params.toList()) + } + + fun assertions(): LambdaRecorderAssertions { + return LambdaRecorderAssertions(parametersSequence = parametersSequence) + } +} + +inline fun lambdaRecorder( + noinline block: () -> R +): LambdaNoParamRecorder { + return LambdaNoParamRecorder(block) +} + +inline fun lambdaRecorder( + noinline block: (T) -> R +): LambdaOneParamRecorder { + return LambdaOneParamRecorder(block) +} + +inline fun lambdaRecorder( + noinline block: (T1, T2) -> R +): LambdaTwoParamsRecorder { + return LambdaTwoParamsRecorder(block) +} + +inline fun lambdaRecorder( + noinline block: (T1, T2, T3) -> R +): LambdaThreeParamsRecorder { + return LambdaThreeParamsRecorder(block) +} + +inline fun lambdaRecorder( + noinline block: (T1, T2, T3, T4) -> R +): LambdaFourParamsRecorder { + return LambdaFourParamsRecorder(block) +} + +class LambdaNoParamRecorder(val block: () -> R) : LambdaRecorder(), () -> R { + override fun invoke(): R { + onInvoke() + return block() + } +} + +class LambdaOneParamRecorder(val block: (T) -> R) : LambdaRecorder(), (T) -> R { + override fun invoke(p: T): R { + onInvoke(p) + return block(p) + } +} + +class LambdaTwoParamsRecorder(val block: (T1, T2) -> R) : LambdaRecorder(), (T1, T2) -> R { + override fun invoke(p1: T1, p2: T2): R { + onInvoke(p1, p2) + return block(p1, p2) + } +} + +class LambdaThreeParamsRecorder(val block: (T1, T2, T3) -> R) : LambdaRecorder(), (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(val block: (T1, T2, T3, T4) -> R) : LambdaRecorder(), (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..b070204d28 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt @@ -0,0 +1,41 @@ +/* + * 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 +} + +/** + * 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 +} From 19523339af36aa2cf69285ca8c8194d51bcf95e7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Mar 2024 14:59:22 +0100 Subject: [PATCH 2/2] Tests : improve lambda recorder --- .../tests/testutils/lambda/Assertions.kt | 26 ++++---------- .../tests/testutils/lambda/LambdaRecorder.kt | 36 +++++++++++++------ .../testutils/lambda/ParameterMatcher.kt | 2 ++ 3 files changed, 34 insertions(+), 30 deletions(-) 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 index 4b08a160ba..af241fdea0 100644 --- 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 @@ -56,29 +56,17 @@ class ParametersAssertions internal constructor( ) { fun withSequence(vararg matchersSequence: List) { if (parametersSequence.size != matchersSequence.size) { - throw AssertionError("Expected ${matchersSequence.size} parameters, but got ${parametersSequence.size} parameters") + throw AssertionError("Lambda was called ${parametersSequence.size} times, but only ${matchersSequence.size} assertions were provided") } - parametersSequence.zip(matchersSequence).forEach { (parameters, matchers) -> + parametersSequence.zip(matchersSequence).forEachIndexed { invocationIndex, (parameters, matchers) -> if (parameters.size != matchers.size) { - throw AssertionError("Expected ${matchers.size} parameters, but got ${parameters.size} parameters") + throw AssertionError("Expected ${matchers.size} parameters, but got ${parameters.size} parameters during invocation #$invocationIndex") } - parameters.zip(matchers).forEachIndexed { j, (param, matcher) -> + parameters.zip(matchers).forEachIndexed { paramIndex, (param, matcher) -> if (!matcher.match(param)) { - throw AssertionError("Parameter $j does not match the expected value") - } - } - } - for (i in parametersSequence.indices) { - val params = parametersSequence[i] - val checker = matchersSequence[i] - if (params.size != checker.size) { - throw AssertionError("Expected ${checker.size} parameters, but got ${params.size} parameters") - } - for (j in params.indices) { - val param = params[j] - val check = checker[j] - if (!check.match(param)) { - throw AssertionError("Parameter $j does not match the expected value") + throw AssertionError( + "Parameter #$paramIndex does not match the expected value (actual=$param,expected=$matcher) during invocation #$invocationIndex" + ) } } } 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 index 52c3dc63a8..b7beaaa5e9 100644 --- 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 @@ -19,10 +19,15 @@ 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() { +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()) } @@ -32,64 +37,73 @@ abstract class LambdaRecorder internal constructor() { } inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, noinline block: () -> R ): LambdaNoParamRecorder { - return LambdaNoParamRecorder(block) + return LambdaNoParamRecorder(ensureNeverCalled, block) } inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, noinline block: (T) -> R ): LambdaOneParamRecorder { - return LambdaOneParamRecorder(block) + return LambdaOneParamRecorder(ensureNeverCalled, block) } inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, noinline block: (T1, T2) -> R ): LambdaTwoParamsRecorder { - return LambdaTwoParamsRecorder(block) + return LambdaTwoParamsRecorder(ensureNeverCalled, block) } inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, noinline block: (T1, T2, T3) -> R ): LambdaThreeParamsRecorder { - return LambdaThreeParamsRecorder(block) + return LambdaThreeParamsRecorder(ensureNeverCalled, block) } inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, noinline block: (T1, T2, T3, T4) -> R ): LambdaFourParamsRecorder { - return LambdaFourParamsRecorder(block) + return LambdaFourParamsRecorder(ensureNeverCalled, block) } -class LambdaNoParamRecorder(val block: () -> R) : LambdaRecorder(), () -> R { +class LambdaNoParamRecorder(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R { override fun invoke(): R { onInvoke() return block() } } -class LambdaOneParamRecorder(val block: (T) -> R) : LambdaRecorder(), (T) -> R { +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(val block: (T1, T2) -> R) : LambdaRecorder(), (T1, T2) -> R { +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(val block: (T1, T2, T3) -> R) : LambdaRecorder(), (T1, T2, T3) -> R { +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(val block: (T1, T2, T3, T4) -> R) : LambdaRecorder(), (T1, T2, T3, T4) -> R { +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 index b070204d28..dd509ed262 100644 --- 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 @@ -30,6 +30,7 @@ interface ParameterMatcher { */ fun value(expectedValue: T) = object : ParameterMatcher { override fun match(param: Any?) = param == expectedValue + override fun toString(): String = "value($expectedValue)" } /** @@ -38,4 +39,5 @@ fun value(expectedValue: T) = object : ParameterMatcher { */ fun any() = object : ParameterMatcher { override fun match(param: Any?) = true + override fun toString(): String = "any()" }