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.
		
		
		
		
		
			
		
			
				
	
	
		
			254 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			254 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
 | |
|     )
 | |
| }
 |