mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			265 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			265 lines
		
	
	
		
			11 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
 | 
						|
) -> Nimble.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 (invalid mock param, not called or 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
 | 
						|
                .map { "\($0.name) (params: \($0.paramCount))" }
 | 
						|
                .filter { $0 != "\(callInfo.functionName) (params: \(callInfo.parameterCount))" }
 | 
						|
            
 | 
						|
            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: [FunctionConsumer.Key]
 | 
						|
    let desiredFunctionCalls: [String]
 | 
						|
    
 | 
						|
    var functionName: String { "\((function?.name).map { "\($0)" } ?? "a function")" }
 | 
						|
    var parameterCount: Int { (function?.parameterCount ?? 0) }
 | 
						|
    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: [FunctionConsumer.Key],
 | 
						|
        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: [FunctionConsumer.Key] = []
 | 
						|
    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, parameterCount, parameterSummary in
 | 
						|
            validInstance.functionConsumer
 | 
						|
                .firstFunction(
 | 
						|
                    for: FunctionConsumer.Key(name: name, paramCount: parameterCount),
 | 
						|
                    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()
 | 
						|
                
 | 
						|
                let key: FunctionConsumer.Key = FunctionConsumer.Key(
 | 
						|
                    name: (maybeFunction?.name ?? ""),
 | 
						|
                    paramCount: (maybeFunction?.parameterCount ?? 0)
 | 
						|
                )
 | 
						|
                desiredFunctionCalls = validInstance.functionConsumer.calls
 | 
						|
                    .wrappedValue[key]
 | 
						|
                    .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
 | 
						|
    )
 | 
						|
}
 |