mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
a41f1c1366
Cleaned up the Dependencies so that tests can run synchronously without having to custom set queues as much Sorted out the crypto and network dependencies to avoid needing weird dependency inheritance Fixed the flaky tests so they are no longer flaky Fixed some unexpected JobRunner behaviours Updated the CI config to use a local build directory for derivedData (now works with build tweaks)
214 lines
7.5 KiB
Swift
214 lines
7.5 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import SessionUtilitiesKit
|
|
|
|
// MARK: - MockError
|
|
|
|
public enum MockError: Error {
|
|
case mockedData
|
|
}
|
|
|
|
// MARK: - Mock<T>
|
|
|
|
public class Mock<T> {
|
|
private let functionHandler: MockFunctionHandler
|
|
internal let functionConsumer: FunctionConsumer
|
|
|
|
// MARK: - Initialization
|
|
|
|
internal required init(functionHandler: MockFunctionHandler? = nil) {
|
|
self.functionConsumer = FunctionConsumer()
|
|
self.functionHandler = (functionHandler ?? self.functionConsumer)
|
|
}
|
|
|
|
// MARK: - MockFunctionHandler
|
|
|
|
@discardableResult internal func accept(funcName: String = #function, args: [Any?] = []) -> Any? {
|
|
return accept(funcName: funcName, checkArgs: args, actionArgs: args)
|
|
}
|
|
|
|
@discardableResult internal func accept(funcName: String = #function, checkArgs: [Any?], actionArgs: [Any?]) -> Any? {
|
|
return functionHandler.accept(
|
|
funcName,
|
|
parameterCount: checkArgs.count,
|
|
parameterSummary: summary(for: checkArgs),
|
|
actionArgs: actionArgs
|
|
)
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
internal func reset() {
|
|
functionConsumer.trackCalls = true
|
|
functionConsumer.functionBuilders = []
|
|
functionConsumer.functionHandlers = [:]
|
|
functionConsumer.calls.mutate { $0 = [:] }
|
|
}
|
|
|
|
internal func when<R>(_ callBlock: @escaping (inout T) throws -> R) -> MockFunctionBuilder<T, R> {
|
|
let builder: MockFunctionBuilder<T, R> = MockFunctionBuilder(callBlock, mockInit: type(of: self).init)
|
|
functionConsumer.functionBuilders.append(builder.build)
|
|
|
|
return builder
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
private func summary(for argument: Any) -> String {
|
|
switch argument {
|
|
case let string as String: return string
|
|
case let array as [Any]: return "[\(array.map { summary(for: $0) }.joined(separator: ", "))]"
|
|
|
|
case let dict as [String: Any]:
|
|
if dict.isEmpty { return "[:]" }
|
|
|
|
let sortedValues: [String] = dict
|
|
.map { key, value in "\(summary(for: key)):\(summary(for: value))" }
|
|
.sorted()
|
|
return "[\(sortedValues.joined(separator: ", "))]"
|
|
|
|
case let data as Data: return "Data(base64Encoded: \(data.base64EncodedString()))"
|
|
|
|
default: return String(reflecting: argument) // Default to the `debugDescription` if available
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - MockFunctionHandler
|
|
|
|
protocol MockFunctionHandler {
|
|
func accept(_ functionName: String, parameterCount: Int, parameterSummary: String, actionArgs: [Any?]) -> Any?
|
|
}
|
|
|
|
// MARK: - MockFunction
|
|
|
|
internal class MockFunction {
|
|
var name: String
|
|
var parameterCount: Int
|
|
var parameterSummary: String
|
|
var actions: [([Any?]) -> Void]
|
|
var returnValue: Any?
|
|
|
|
init(name: String, parameterCount: Int, parameterSummary: String, actions: [([Any?]) -> Void], returnValue: Any?) {
|
|
self.name = name
|
|
self.parameterCount = parameterCount
|
|
self.parameterSummary = parameterSummary
|
|
self.actions = actions
|
|
self.returnValue = returnValue
|
|
}
|
|
}
|
|
|
|
// MARK: - MockFunctionBuilder
|
|
|
|
internal class MockFunctionBuilder<T, R>: MockFunctionHandler {
|
|
private let callBlock: (inout T) throws -> R
|
|
private let mockInit: (MockFunctionHandler?) -> Mock<T>
|
|
private var functionName: String?
|
|
private var parameterCount: Int?
|
|
private var parameterSummary: String?
|
|
private var actions: [([Any?]) -> Void] = []
|
|
private var returnValue: R?
|
|
internal var returnValueGenerator: ((String, Int, String) -> R?)?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(_ callBlock: @escaping (inout T) throws -> R, mockInit: @escaping (MockFunctionHandler?) -> Mock<T>) {
|
|
self.callBlock = callBlock
|
|
self.mockInit = mockInit
|
|
}
|
|
|
|
// MARK: - Behaviours
|
|
|
|
@discardableResult func then(_ block: @escaping ([Any?]) -> Void) -> MockFunctionBuilder<T, R> {
|
|
actions.append(block)
|
|
return self
|
|
}
|
|
|
|
func thenReturn(_ value: R?) {
|
|
returnValue = value
|
|
}
|
|
|
|
// MARK: - MockFunctionHandler
|
|
|
|
func accept(_ functionName: String, parameterCount: Int, parameterSummary: String, actionArgs: [Any?]) -> Any? {
|
|
self.functionName = functionName
|
|
self.parameterCount = parameterCount
|
|
self.parameterSummary = parameterSummary
|
|
return (returnValue ?? returnValueGenerator?(functionName, parameterCount, parameterSummary))
|
|
}
|
|
|
|
// MARK: - Build
|
|
|
|
func build() throws -> MockFunction {
|
|
var completionMock = mockInit(self) as! T
|
|
_ = try? callBlock(&completionMock)
|
|
|
|
guard let name: String = functionName, let parameterCount: Int = parameterCount, let parameterSummary: String = parameterSummary else {
|
|
preconditionFailure("Attempted to build the MockFunction before it was called")
|
|
}
|
|
|
|
return MockFunction(name: name, parameterCount: parameterCount, parameterSummary: parameterSummary, actions: actions, returnValue: returnValue)
|
|
}
|
|
}
|
|
|
|
// MARK: - FunctionConsumer
|
|
|
|
internal class FunctionConsumer: MockFunctionHandler {
|
|
struct Key: Equatable, Hashable {
|
|
let name: String
|
|
let paramCount: Int
|
|
}
|
|
|
|
var trackCalls: Bool = true
|
|
var functionBuilders: [() throws -> MockFunction?] = []
|
|
var functionHandlers: [Key: [String: MockFunction]] = [:]
|
|
var calls: Atomic<[Key: [String]]> = Atomic([:])
|
|
|
|
func accept(_ functionName: String, parameterCount: Int, parameterSummary: String, actionArgs: [Any?]) -> Any? {
|
|
let key: Key = Key(name: functionName, paramCount: parameterCount)
|
|
|
|
if !functionBuilders.isEmpty {
|
|
functionBuilders
|
|
.compactMap { try? $0() }
|
|
.forEach { function in
|
|
let key: Key = Key(name: function.name, paramCount: function.parameterCount)
|
|
|
|
functionHandlers[key] = (functionHandlers[key] ?? [:])
|
|
.setting(function.parameterSummary, function)
|
|
}
|
|
|
|
functionBuilders.removeAll()
|
|
}
|
|
|
|
guard let expectation: MockFunction = firstFunction(for: key, matchingParameterSummaryIfPossible: parameterSummary) else {
|
|
preconditionFailure("No expectations found for \(functionName)")
|
|
}
|
|
|
|
// Record the call so it can be validated later (assuming we are tracking calls)
|
|
if trackCalls {
|
|
calls.mutate { $0[key] = ($0[key] ?? []).appending(parameterSummary) }
|
|
}
|
|
|
|
for action in expectation.actions {
|
|
action(actionArgs)
|
|
}
|
|
|
|
return expectation.returnValue
|
|
}
|
|
|
|
func firstFunction(for key: Key, matchingParameterSummaryIfPossible parameterSummary: String) -> MockFunction? {
|
|
guard let possibleExpectations: [String: MockFunction] = functionHandlers[key] else { return nil }
|
|
|
|
guard let expectation: MockFunction = possibleExpectations[parameterSummary] else {
|
|
// A `nil` response might be value but in a lot of places we will need to force-cast
|
|
// so try to find a non-nil response first
|
|
return (
|
|
possibleExpectations.values.first(where: { $0.returnValue != nil }) ??
|
|
possibleExpectations.values.first
|
|
)
|
|
}
|
|
|
|
return expectation
|
|
}
|
|
}
|