commonMain.org.kodein.mock.Mocker.kt Maven / Gradle / Ivy
package org.kodein.mock
private typealias RegistrationMap = HashMap, MutableList>, E>>>
public class Mocker {
public class MockingException(message: String) : Exception(message)
private sealed class SpecialMode {
object DEFINITION : SpecialMode()
class VERIFICATION(val exhaustive: Boolean, val inOrder: Boolean, references: References) : SpecialMode() {
val builder = ArgConstraintsBuilder(references)
}
}
private var specialMode: SpecialMode? = null
internal class CallDefinition(val isSuspend: Boolean, val receiver: Any?, val method: String, val args: Array<*>) : RuntimeException("This exception should have been caught!")
private val regFuns = RegistrationMap>()
private val regSuspendFuns = RegistrationMap>()
@Suppress("ArrayInDataClass")
private data class Call(val receiver: Any?, val method: String, val arguments: Array<*>, val returnValue: Any?)
private val calls = ArrayDeque()
private val references = References()
public fun clearCalls() { calls.clear() }
public fun reset() {
calls.clear()
regFuns.clear()
regSuspendFuns.clear()
}
private fun methodName(receiver: Any?, methodName: String) = if (receiver == null) methodName else "${receiver::class.simpleName}.$methodName"
private sealed class ProcessResult {
class Value(val value: R) : ProcessResult()
object FromRegistration : ProcessResult()
}
private fun process(isSuspend: Boolean, receiver: Any?, method: String, args: Array<*>, regs: RegistrationMap): ProcessResult {
when (val mode = specialMode) {
is SpecialMode.DEFINITION -> {
throw CallDefinition(isSuspend, receiver, method, args)
}
is SpecialMode.VERIFICATION -> {
val constraints = mode.builder.getConstraints(args)
val list = regs[receiver to method] ?: throw MockingException("Cannot verify ${methodName(receiver, method)} as it has not been mocked")
list.firstOrNull { (constraints, _) ->
constraints.size == args.size && constraints.indices.all {
@Suppress("UNCHECKED_CAST")
(constraints[it] as ArgConstraint).isValid(args[it])
}
} ?: throw MockingException(
"Cannot verify ${methodName(receiver, method)} as it has not been mocked for arguments ${args.joinToString()}\n" +
" Registered mocked:\n" + list.map { it.first.joinToString { it.description } } .joinToString("\n") { " $it" }
)
val call = if (mode.exhaustive && mode.inOrder) {
val call = calls.removeFirstOrNull()
?: throw AssertionError("Expected a call to ${methodName(receiver, method)} but call list was empty")
if (method != call.method || receiver !== receiver)
throw AssertionError("Expected a call to ${methodName(receiver, method)}, but was a call to ${methodName(call.receiver, call.method)}")
if (constraints.size != call.arguments.size)
throw AssertionError("Expected ${constraints.size} arguments to ${methodName(receiver, method)} but got ${call.arguments.size}")
@Suppress("UNCHECKED_CAST")
constraints.forEachIndexed { i, constraint -> (constraint as ArgConstraint).assert("Argument ${i + 1}", call.arguments[i]) }
call
} else {
val callIndices = (
calls.indices.filter { calls[it].receiver == receiver && calls[it].method == method } .takeIf { it.isNotEmpty() }
?: throw AssertionError("Could not find a call to ${methodName(receiver, method)}")
).filter { calls[it].arguments.size == constraints.size } .takeIf { it.isNotEmpty() }
?: throw AssertionError("Could not find a call to ${methodName(receiver, method)} with ${constraints.size} arguments")
val callIndex = if (callIndices.size == 1) {
val call = calls[callIndices.single()]
@Suppress("UNCHECKED_CAST")
constraints.forEachIndexed { i, constraint -> (constraint as ArgConstraint).assert("Argument ${i + 1}", call.arguments[i]) }
callIndices.single()
} else {
@Suppress("UNCHECKED_CAST")
callIndices.firstOrNull { callIndex -> constraints.indices.all { (constraints[it] as ArgConstraint).isValid(calls[callIndex].arguments[it]) } }
?: throw AssertionError("Found ${callIndices.size} calls to ${methodName(receiver, method)}, but none that validates the constraints")
}
val call = calls[callIndex]
if (mode.inOrder) repeat(callIndex + 1) { calls.removeFirst() }
else calls.removeAt(callIndex)
call
}
@Suppress("UNCHECKED_CAST")
constraints.forEachIndexed { i, constraint -> (constraint.capture as MutableList?)?.add(call.arguments[i]) }
@Suppress("UNCHECKED_CAST")
return ProcessResult.Value(call.returnValue as R)
}
null -> {
@Suppress("UNCHECKED_CAST")
return ProcessResult.FromRegistration as ProcessResult
}
}
}
@Suppress("UNCHECKED_CAST")
private inline fun registerImpl(isSuspend: Boolean, regs: RegistrationMap, run: E.(Array<*>) -> Any?, receiver: Any?, method: String, args: Array<*>, @Suppress("UNUSED_PARAMETER") noinline default: (() -> R)?): R {
when (val result = process(isSuspend, receiver, method, args, regs)) {
is ProcessResult.Value -> return result.value
is ProcessResult.FromRegistration -> {
val list = regs[receiver to method]
val pair = list?.firstOrNull { (constraints, _) ->
constraints.size == args.size && constraints.indices.all {
(constraints[it] as ArgConstraint).isValid(args[it])
}
}
return when {
pair != null -> {
val (constraints, every) = pair
args.forEachIndexed { i, a -> (constraints[i].capture as? MutableList)?.add(a) }
val ret = every.run(args)
calls.addLast(Call(receiver, method, args, ret))
ret as R
}
default != null -> default()
else -> {
if (list != null) {
throw MockingException(
"${methodName(receiver, method)} has not been mocked for arguments ${args.joinToString()}\n" +
" Registered mocked:\n" + list.map { it.first.joinToString { it.description } } .joinToString("\n") { " $it" }
)
} else {
throw MockingException("${methodName(receiver, method)} has not been mocked")
}
}
}
}
}
}
public fun register(receiver: Any?, method: String, vararg args: Any?, default: (() -> R)? = null): R =
registerImpl(false, regFuns, { mocked(it) }, receiver, method, args, default)
public suspend fun registerSuspend(receiver: Any?, method: String, vararg args: Any?, default: (() -> R)? = null): R =
registerImpl(true, regSuspendFuns, { mocked(it) }, receiver, method, args, default)
public inner class Every internal constructor(receiver: Any?, method: String) {
internal var mocked: (Array<*>) -> T = { throw MockingException("${methodName(receiver, method)} has not been mocked") }
public infix fun returns(ret: T) {
mocked = { ret }
}
public infix fun runs(ret: (Array<*>) -> T) {
mocked = ret
}
}
public inner class EverySuspend internal constructor(receiver: Any?, method: String) {
internal var mocked: suspend (Array<*>) -> T = { throw MockingException("${methodName(receiver, method)} has not been mocked") }
public infix fun returns(ret: T) {
mocked = { ret }
}
public infix fun runs(ret: suspend (Array<*>) -> T) {
mocked = ret
}
}
// This will be inlined twice: once for regular functions, and once for suspend functions.
private inline fun everyImpl(isSuspend: Boolean, newEvery: (Any?, String) -> ET, map: RegistrationMap, block: ArgConstraintsBuilder.() -> T): ET {
if (specialMode != null) error("Cannot be inside a definition block AND a verification block")
specialMode = SpecialMode.DEFINITION
val builder = ArgConstraintsBuilder(references)
try {
builder.block()
error("Expected a Mock call")
} catch (call: CallDefinition) {
if (call.isSuspend != isSuspend) error("Calling a ${if (call.isSuspend) "suspend" else "non suspend"} function inside a ${if (isSuspend) "suspending" else "non suspending"} every block")
val every = newEvery(call.receiver, call.method)
map.getOrPut(call.receiver to call.method) { ArrayList() }
.add(builder.getConstraints(call.args) to every)
return every
} finally {
specialMode = null
}
}
public fun every(block: ArgConstraintsBuilder.() -> T) : Every =
everyImpl(false, ::Every, regFuns) { block() }
public suspend fun everySuspending(block: suspend ArgConstraintsBuilder.() -> T): EverySuspend =
everyImpl(true, ::EverySuspend, regSuspendFuns) { block() }
@Deprecated("Renamed every", ReplaceWith("every(block)"), level = DeprecationLevel.ERROR)
public fun on(block: ArgConstraintsBuilder.() -> T) : Every = every(block)
// This will be inlined twice: once for regular functions, and once for suspend functions.
private inline fun verifyImpl(exhaustive: Boolean, inOrder: Boolean, block: ArgConstraintsBuilder.() -> Unit) {
if (specialMode != null) error("Cannot be inside a definition block AND a verification block")
val mode = SpecialMode.VERIFICATION(exhaustive, inOrder, references)
specialMode = mode
try {
mode.builder.block()
if (exhaustive && calls.isNotEmpty()) {
val call = calls.first()
throw AssertionError("Expected call list to be empty, but got a call to ${methodName(call.receiver, call.method)}")
} else {
calls.clear()
}
} finally {
specialMode = null
}
}
public fun verify(exhaustive: Boolean = true, inOrder: Boolean = true, block: ArgConstraintsBuilder.() -> Unit): Unit =
verifyImpl(exhaustive, inOrder) { block() }
public suspend fun verifyWithSuspend(exhaustive: Boolean = true, inOrder: Boolean = true, block: suspend ArgConstraintsBuilder.() -> Unit): Unit =
verifyImpl(exhaustive, inOrder) { block() }
public fun useReference(r: Any) {
references.addReference(r)
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy