mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
27e0981913
Added a toast when hitting the emoji reacts rate limit Added the ability to delete info messages Fixed some odd layout behaviours with the VisibleMessageCell Fixed some layout issues with reactions Removed some unneeded custom code
253 lines
10 KiB
Swift
253 lines
10 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import Nimble
|
|
import SessionUtilitiesKit
|
|
|
|
public enum CallAmount {
|
|
case atLeast(times: Int)
|
|
case exactly(times: Int)
|
|
case noMoreThan(times: Int)
|
|
}
|
|
|
|
fileprivate func timeStr(_ value: Int) -> String {
|
|
return "\(value) time\(value == 1 ? "" : "s")"
|
|
}
|
|
|
|
/// Validates whether the function called in `functionBlock` has been called according to the parameter constraints
|
|
///
|
|
/// - Parameters:
|
|
/// - amount: An enum constraining the number of times the function can be called (Default is `.atLeast(times: 1)`
|
|
///
|
|
/// - matchingParameters: A boolean indicating whether the parameters for the function call need to match exactly
|
|
///
|
|
/// - exclusive: A boolean indicating whether no other functions should be called
|
|
///
|
|
/// - functionBlock: A closure in which the function to be validated should be called
|
|
public func call<M, T, R>(
|
|
_ amount: CallAmount = .atLeast(times: 1),
|
|
matchingParameters: Bool = false,
|
|
exclusive: Bool = false,
|
|
functionBlock: @escaping (inout T) throws -> R
|
|
) -> Predicate<M> where M: Mock<T> {
|
|
return Predicate.define { actualExpression in
|
|
let callInfo: CallInfo = generateCallInfo(actualExpression, functionBlock)
|
|
let matchingParameterRecords: [String] = callInfo.desiredFunctionCalls
|
|
.filter { !matchingParameters || callInfo.hasMatchingParameters($0) }
|
|
let exclusiveCallsValid: Bool = (!exclusive || callInfo.allFunctionsCalled.count <= 1) // '<=' to support '0' case
|
|
let (numParamMatchingCallsValid, timesError): (Bool, String?) = {
|
|
switch amount {
|
|
case .atLeast(let times):
|
|
return (
|
|
(matchingParameterRecords.count >= times),
|
|
(times <= 1 ? nil : "at least \(timeStr(times))")
|
|
)
|
|
|
|
case .exactly(let times):
|
|
return (
|
|
(matchingParameterRecords.count == times),
|
|
"exactly \(timeStr(times))"
|
|
)
|
|
|
|
case .noMoreThan(let times):
|
|
return (
|
|
(matchingParameterRecords.count <= times),
|
|
(times <= 0 ? nil : "no more than \(timeStr(times))")
|
|
)
|
|
}
|
|
}()
|
|
|
|
let result = (
|
|
numParamMatchingCallsValid &&
|
|
exclusiveCallsValid
|
|
)
|
|
let matchingParametersError: String? = (matchingParameters ?
|
|
"matching the parameters\(callInfo.desiredParameters.map { ": \($0)" } ?? "")" :
|
|
nil
|
|
)
|
|
let distinctParameterCombinations: Set<String> = Set(callInfo.desiredFunctionCalls)
|
|
let actualMessage: String
|
|
|
|
if callInfo.caughtException != nil {
|
|
actualMessage = "a thrown assertion (might not have been called or has no mocked return value)"
|
|
}
|
|
else if callInfo.function == nil {
|
|
actualMessage = "no call details"
|
|
}
|
|
else if callInfo.desiredFunctionCalls.isEmpty {
|
|
actualMessage = "no calls"
|
|
}
|
|
else if !exclusiveCallsValid {
|
|
let otherFunctionsCalled: [String] = callInfo.allFunctionsCalled.filter { $0 != callInfo.functionName }
|
|
|
|
actualMessage = "calls to other functions: [\(otherFunctionsCalled.joined(separator: ", "))]"
|
|
}
|
|
else {
|
|
let onlyMadeMatchingCalls: Bool = (matchingParameterRecords.count == callInfo.desiredFunctionCalls.count)
|
|
|
|
switch (numParamMatchingCallsValid, onlyMadeMatchingCalls, distinctParameterCombinations.count) {
|
|
case (false, false, 1):
|
|
// No calls with the matching parameter requirements but only one parameter combination
|
|
// so include the param info
|
|
actualMessage = "called \(timeStr(callInfo.desiredFunctionCalls.count)) with different parameters: \(callInfo.desiredFunctionCalls[0])"
|
|
|
|
case (false, true, _):
|
|
actualMessage = "called \(timeStr(callInfo.desiredFunctionCalls.count))"
|
|
|
|
case (false, false, _):
|
|
let distinctSetterCombinations: Set<String> = distinctParameterCombinations.filter { $0 != "[]" }
|
|
|
|
// A getter/setter combo will have function calls split between no params and the set value
|
|
// if the setter didn't match then we still want to show the incorrect parameters
|
|
if distinctSetterCombinations.count == 1, let paramCombo: String = distinctSetterCombinations.first {
|
|
actualMessage = "called with: \(paramCombo)"
|
|
}
|
|
else {
|
|
actualMessage = "called \(timeStr(matchingParameterRecords.count)) with matching parameters, \(timeStr(callInfo.desiredFunctionCalls.count)) total"
|
|
}
|
|
|
|
default: actualMessage = "\(exclusive ? " exclusive " : "")call to '\(callInfo.functionName)'"
|
|
}
|
|
}
|
|
|
|
return PredicateResult(
|
|
bool: result,
|
|
message: .expectedCustomValueTo(
|
|
[
|
|
"call '\(callInfo.functionName)'\(exclusive ? " exclusively" : "")",
|
|
timesError,
|
|
matchingParametersError
|
|
]
|
|
.compactMap { $0 }
|
|
.joined(separator: " "),
|
|
actual: actualMessage
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Shared Code
|
|
|
|
fileprivate struct CallInfo {
|
|
let didError: Bool
|
|
let caughtException: BadInstructionException?
|
|
let function: MockFunction?
|
|
let allFunctionsCalled: [String]
|
|
let desiredFunctionCalls: [String]
|
|
|
|
var functionName: String { "\((function?.name).map { "\($0)" } ?? "a function")" }
|
|
var desiredParameters: String? { function?.parameterSummary }
|
|
|
|
static var error: CallInfo {
|
|
CallInfo(
|
|
didError: true,
|
|
caughtException: nil,
|
|
function: nil,
|
|
allFunctionsCalled: [],
|
|
desiredFunctionCalls: []
|
|
)
|
|
}
|
|
|
|
init(
|
|
didError: Bool = false,
|
|
caughtException: BadInstructionException?,
|
|
function: MockFunction?,
|
|
allFunctionsCalled: [String],
|
|
desiredFunctionCalls: [String]
|
|
) {
|
|
self.didError = didError
|
|
self.caughtException = caughtException
|
|
self.function = function
|
|
self.allFunctionsCalled = allFunctionsCalled
|
|
self.desiredFunctionCalls = desiredFunctionCalls
|
|
}
|
|
|
|
func hasMatchingParameters(_ parameters: String) -> Bool {
|
|
return (parameters == (function?.parameterSummary ?? "FALLBACK_NOT_FOUND"))
|
|
}
|
|
}
|
|
|
|
fileprivate func generateCallInfo<M, T, R>(_ actualExpression: Expression<M>, _ functionBlock: @escaping (inout T) throws -> R) -> CallInfo where M: Mock<T> {
|
|
var maybeFunction: MockFunction?
|
|
var allFunctionsCalled: [String] = []
|
|
var desiredFunctionCalls: [String] = []
|
|
let builderCreator: ((M) -> MockFunctionBuilder<T, R>) = { validInstance in
|
|
let builder: MockFunctionBuilder<T, R> = MockFunctionBuilder(functionBlock, mockInit: type(of: validInstance).init)
|
|
builder.returnValueGenerator = { name, parameterSummary in
|
|
validInstance.functionConsumer
|
|
.firstFunction(for: name, matchingParameterSummaryIfPossible: parameterSummary)?
|
|
.returnValue as? R
|
|
}
|
|
|
|
return builder
|
|
}
|
|
|
|
#if (arch(x86_64) || arch(arm64)) && (canImport(Darwin) || canImport(Glibc))
|
|
var didError: Bool = false
|
|
let caughtException: BadInstructionException? = catchBadInstruction {
|
|
do {
|
|
guard let validInstance: M = try actualExpression.evaluate() else {
|
|
didError = true
|
|
return
|
|
}
|
|
|
|
allFunctionsCalled = Array(validInstance.functionConsumer.calls.wrappedValue.keys)
|
|
|
|
// Only check for the specific function calls if there was at least a single
|
|
// call (if there weren't any this will likely throw errors when attempting
|
|
// to build)
|
|
if !allFunctionsCalled.isEmpty {
|
|
let builder: MockFunctionBuilder<T, R> = builderCreator(validInstance)
|
|
validInstance.functionConsumer.trackCalls = false
|
|
maybeFunction = try? builder.build()
|
|
desiredFunctionCalls = validInstance.functionConsumer.calls
|
|
.wrappedValue[maybeFunction?.name ?? ""]
|
|
.defaulting(to: [])
|
|
validInstance.functionConsumer.trackCalls = true
|
|
}
|
|
else {
|
|
desiredFunctionCalls = []
|
|
}
|
|
}
|
|
catch {
|
|
didError = true
|
|
}
|
|
}
|
|
|
|
// Make sure to switch this back on in case an assertion was thrown (which would meant this
|
|
// wouldn't have been reset)
|
|
(try? actualExpression.evaluate())?.functionConsumer.trackCalls = true
|
|
|
|
guard !didError else { return CallInfo.error }
|
|
#else
|
|
let caughtException: BadInstructionException? = nil
|
|
|
|
// Just hope for the best and if there is a force-cast there's not much we can do
|
|
guard let validInstance: M = try? actualExpression.evaluate() else { return CallInfo.error }
|
|
|
|
allFunctionsCalled = Array(validInstance.functionConsumer.calls.wrappedValue.keys)
|
|
|
|
// Only check for the specific function calls if there was at least a single
|
|
// call (if there weren't any this will likely throw errors when attempting
|
|
// to build)
|
|
if !allFunctionsCalled.isEmpty {
|
|
let builder: MockExpectationBuilder<T, R> = builderCreator(validInstance)
|
|
validInstance.functionConsumer.trackCalls = false
|
|
maybeFunction = try? builder.build()
|
|
desiredFunctionCalls = validInstance.functionConsumer.calls
|
|
.wrappedValue[maybeFunction?.name ?? ""]
|
|
.defaulting(to: [])
|
|
validInstance.functionConsumer.trackCalls = true
|
|
}
|
|
else {
|
|
desiredFunctionCalls = []
|
|
}
|
|
#endif
|
|
|
|
return CallInfo(
|
|
caughtException: caughtException,
|
|
function: maybeFunction,
|
|
allFunctionsCalled: allFunctionsCalled,
|
|
desiredFunctionCalls: desiredFunctionCalls
|
|
)
|
|
}
|